Authentication Guide

Comprehensive guide to implementing authentication with GreenMonkey's API and OAuth providers.

Authentication Methods

GreenMonkey supports multiple authentication methods:

  1. API Key Authentication - For server-to-server communication
  2. OAuth 2.0 - For user authentication via GitHub/Google
  3. Session Tokens - For web application sessions
  4. Magic Links - Passwordless email authentication

API Key Authentication

Obtaining API Keys

  1. Sign in to GreenMonkey
  2. Navigate to Dashboard → Settings → API
  3. Click "Generate API Key"
  4. Set permissions and name
  5. Copy key immediately (shown once!)

Using API Keys

Header Authentication (Recommended)

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://api.greenmonkey.dev/v1/products

Query Parameter (Not Recommended)

curl https://api.greenmonkey.dev/v1/products?api_key=YOUR_API_KEY

āš ļø Warning: Query parameters may be logged. Use headers instead.

API Key Security

// Good: Environment variable
const apiKey = process.env.GREENMONKEY_API_KEY;

// Bad: Hardcoded
const apiKey = 'gm_live_abc123...'; // Never do this!

// Good: Secure storage
import { SecretManager } from '@google-cloud/secret-manager';
const apiKey = await secretManager.getSecret('greenmonkey-api-key');

API Key Permissions

Configure granular permissions:

{
  "permissions": {
    "products:read": true,
    "products:write": true,
    "purchases:read": true,
    "purchases:write": false,
    "analytics:read": true,
    "ai:execute": true
  }
}

OAuth 2.0 Authentication

OAuth Flow Overview

sequenceDiagram
    participant User
    participant YourApp
    participant GreenMonkey
    participant Provider

    User->>YourApp: Click "Sign in with GitHub"
    YourApp->>GreenMonkey: Redirect to /api/auth/github
    GreenMonkey->>Provider: OAuth authorization
    Provider->>User: Consent screen
    User->>Provider: Approve
    Provider->>GreenMonkey: Authorization code
    GreenMonkey->>Provider: Exchange for tokens
    GreenMonkey->>YourApp: Redirect with session
    YourApp->>User: Authenticated!

Implementing OAuth

GitHub OAuth

  1. Register Your App

    https://github.com/settings/applications/new
    
    Application name: Your App Name
    Homepage URL: https://yourapp.com
    Authorization callback URL: https://yourapp.com/api/auth/callback/github
    
  2. Configure Environment

    GITHUB_CLIENT_ID=your_client_id
    GITHUB_CLIENT_SECRET=your_client_secret
    
  3. Implement Flow

    // Using NextAuth.js
    import NextAuth from 'next-auth';
    import GithubProvider from 'next-auth/providers/github';
    
    export default NextAuth({
      providers: [
        GithubProvider({
          clientId: process.env.GITHUB_CLIENT_ID,
          clientSecret: process.env.GITHUB_CLIENT_SECRET,
        }),
      ],
      callbacks: {
        async signIn({ user, account }) {
          // Custom logic after sign in
          return true;
        },
      },
    });
    

Google OAuth

  1. Create OAuth Client

    https://console.cloud.google.com/apis/credentials
    
    Application type: Web application
    Authorized JavaScript origins: https://yourapp.com
    Authorized redirect URIs: https://yourapp.com/api/auth/callback/google
    
  2. Configure Scopes

    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorization: {
        params: {
          scope: 'openid email profile',
        },
      },
    });
    

OAuth Best Practices

  1. State Parameter - Always use for CSRF protection
  2. PKCE - Implement for public clients
  3. Scope Limitation - Request minimal scopes
  4. Token Storage - Never store in localStorage

Session Management

Session Token Format

GreenMonkey uses JWT tokens for sessions:

// Decoded JWT payload
{
  "sub": "user_123",
  "email": "user@example.com",
  "name": "John Doe",
  "iat": 1616239022,
  "exp": 1616842822
}

Working with Sessions

Client-Side

import { useSession } from 'next-auth/react';

function MyComponent() {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return <p>Loading...</p>;
  }

  if (status === 'unauthenticated') {
    return <p>Please sign in</p>;
  }

  return <p>Welcome, {session.user.name}!</p>;
}

Server-Side

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function GET(request) {
  const session = await getServerSession(authOptions);

  if (!session) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Authenticated logic here
  return new Response(JSON.stringify({ user: session.user }));
}

Session Security

// Configure secure sessions
export const authOptions = {
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  cookies: {
    sessionToken: {
      name: `__Secure-gm.session-token`,
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: true,
      },
    },
  },
};

Magic Link Authentication

Implementation

// Send magic link
async function sendMagicLink(email) {
  const response = await fetch('/api/auth/magic-link', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  });

  if (response.ok) {
    return { success: true, message: 'Check your email!' };
  }
}

// Verify magic link token
async function verifyMagicLink(token) {
  const response = await fetch('/api/auth/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token }),
  });

  if (response.ok) {
    const { sessionToken } = await response.json();
    // Store session token securely
  }
}

Magic Link Security

  • Tokens expire in 10 minutes
  • Single use only
  • Rate limited per email
  • Secure random generation

Multi-Factor Authentication (2FA)

Enabling 2FA

// Generate 2FA secret
import { authenticator } from 'otplib';

const secret = authenticator.generateSecret();
const qrCode = authenticator.keyuri(user.email, 'GreenMonkey', secret);

// Verify TOTP code
const isValid = authenticator.verify({
  token: userInput,
  secret: user.totpSecret,
});

2FA Flow

// During login
async function loginWith2FA(email, password, totpCode) {
  // First, verify credentials
  const user = await verifyCredentials(email, password);

  if (user.has2FA) {
    // Then verify TOTP
    const validTOTP = await verify2FA(user.id, totpCode);
    if (!validTOTP) {
      throw new Error('Invalid 2FA code');
    }
  }

  // Create session
  return createSession(user);
}

Authorization

Role-Based Access Control (RBAC)

// Define roles
const roles = {
  user: ['read:own_profile', 'create:products'],
  seller: ['read:analytics', 'manage:products'],
  admin: ['manage:all', 'view:admin_panel'],
};

// Check permissions
function hasPermission(user, permission) {
  const userRoles = user.roles || ['user'];
  return userRoles.some((role) => roles[role]?.includes(permission));
}

// Protect routes
function requirePermission(permission) {
  return async (req, res, next) => {
    const session = await getSession({ req });

    if (!session || !hasPermission(session.user, permission)) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

Resource-Based Authorization

// Check resource ownership
async function canAccessProduct(userId, productId) {
  const product = await db.product.findUnique({
    where: { id: productId },
  });

  return product?.authorId === userId;
}

// Middleware
async function authorizeProductAccess(req, res, next) {
  const session = await getSession({ req });
  const { productId } = req.params;

  if (!(await canAccessProduct(session.user.id, productId))) {
    return res.status(403).json({ error: 'Access denied' });
  }

  next();
}

Security Best Practices

Token Storage

// Good: HttpOnly cookies
res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`);

// Good: Secure session storage
sessionStorage.setItem('tempToken', token); // Cleared on tab close

// Bad: localStorage (XSS vulnerable)
localStorage.setItem('token', token); // Avoid this!

CSRF Protection

// Generate CSRF token
import { randomBytes } from 'crypto';

function generateCSRFToken() {
  return randomBytes(32).toString('hex');
}

// Verify CSRF token
function verifyCSRFToken(req) {
  const token = req.headers['x-csrf-token'];
  const sessionToken = req.session.csrfToken;

  return token === sessionToken;
}

Rate Limiting

import rateLimit from 'express-rate-limit';

// Auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 requests
  message: 'Too many login attempts',
});

app.post('/api/auth/login', authLimiter, loginHandler);

Testing Authentication

Unit Tests

import { describe, it, expect } from 'vitest';
import { verifyApiKey, generateToken } from '@/lib/auth';

describe('Authentication', () => {
  it('should verify valid API key', async () => {
    const key = 'gm_test_valid_key';
    const result = await verifyApiKey(key);
    expect(result.valid).toBe(true);
  });

  it('should reject invalid API key', async () => {
    const key = 'invalid_key';
    const result = await verifyApiKey(key);
    expect(result.valid).toBe(false);
  });
});

Integration Tests

import { test, expect } from '@playwright/test';

test('OAuth login flow', async ({ page }) => {
  await page.goto('/login');
  await page.click('button:has-text("Sign in with GitHub")');

  // Handle OAuth provider page
  await page.fill('input[name="login"]', process.env.TEST_GITHUB_USER);
  await page.fill('input[name="password"]', process.env.TEST_GITHUB_PASS);
  await page.click('input[type="submit"]');

  // Should redirect back authenticated
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('text=Welcome')).toBeVisible();
});

Troubleshooting

Common Issues

"Invalid API Key"

  • Check for typos or extra spaces
  • Verify key hasn't expired
  • Ensure correct environment (test vs production)

"Session Expired"

  • Implement refresh token logic
  • Extend session duration if needed
  • Handle gracefully in UI

"OAuth Callback Error"

  • Verify redirect URI matches exactly
  • Check OAuth app settings
  • Ensure secrets are correct

Debug Mode

// Enable auth debugging
export const authOptions = {
  debug: process.env.NODE_ENV === 'development',
  logger: {
    error: (code, metadata) => {
      console.error('Auth error:', code, metadata);
    },
    warn: (code) => {
      console.warn('Auth warning:', code);
    },
  },
};

Migration Guide

From v4 to v5 (NextAuth)

// Old (v4)
import { getSession } from 'next-auth/react';
const session = await getSession({ req });

// New (v5)
import { auth } from '@/lib/auth';
const session = await auth();

Custom Authentication

If migrating from custom auth:

  1. Map user schema to NextAuth schema
  2. Migrate session handling
  3. Update API endpoints
  4. Test thoroughly

Next Steps