Skip to main content
Testing authentication is critical for MCP servers that require authorization. The auth fixture provides tools for creating test tokens, testing expiration, and validating scope enforcement.

Why Test Authentication?

Authentication tests verify that your server:
  • Rejects requests without valid tokens
  • Accepts requests with valid tokens
  • Enforces token expiration
  • Validates token signatures
  • Respects scope-based permissions

Creating Test Tokens

The auth fixture generates real JWT tokens using RS256 signing:
test('basic token creation', async ({ mcp, auth }) => {
  const token = await auth.createToken({
    sub: 'user-123',           // Subject (user ID)
    scopes: ['read', 'write'], // OAuth scopes
    email: 'user@example.com', // Email claim
    name: 'John Doe',          // Name claim
  });

  await mcp.authenticate(token);

  // Now all requests include this token
  const tools = await mcp.tools.list();
  expect(tools.length).toBeGreaterThan(0);
});

Token Options

OptionTypeRequiredDescription
substringYesSubject (user identifier)
scopesstring[]NoOAuth scopes
emailstringNoEmail claim
namestringNoName claim
claimsRecord<string, unknown>NoAdditional custom claims
expiresInnumberNoToken lifetime in seconds (default: 3600)

Custom Claims

Add any custom claims your server needs:
const token = await auth.createToken({
  sub: 'user-123',
  claims: {
    tenantId: 'tenant-abc',
    role: 'admin',
    permissions: ['users:read', 'users:write'],
    metadata: { region: 'us-east' },
  },
});

Pre-built Test Users

The auth fixture includes pre-configured test users for common scenarios:
test('using pre-built users', async ({ mcp, auth }) => {
  // Admin with full permissions
  const adminToken = await auth.createToken(auth.users.admin);

  // Regular user
  const userToken = await auth.createToken(auth.users.user);

  // Read-only user
  const readOnlyToken = await auth.createToken(auth.users.readOnly);
});

User Definitions

UsersubScopes
admintest-admin['*'] (all scopes)
usertest-user['read', 'write']
readOnlytest-readonly['read']

Testing Token Expiration

Expired Tokens

test('rejects expired tokens', async ({ mcp, auth }) => {
  const expiredToken = await auth.createExpiredToken({
    sub: 'user-123',
  });

  await expect(mcp.authenticate(expiredToken))
    .rejects.toThrow('expired');
});

Short-Lived Tokens

test('token expires after time limit', async ({ mcp, auth }) => {
  // Token expires in 1 second
  const token = await auth.createToken({
    sub: 'user-123',
    expiresIn: 1,
  });

  await mcp.authenticate(token);

  // First request succeeds
  await mcp.tools.list();

  // Wait for expiration
  await new Promise(resolve => setTimeout(resolve, 1500));

  // Next request fails
  await expect(mcp.tools.list())
    .rejects.toThrow('expired');
});

Testing Invalid Tokens

Invalid Signature

test('rejects invalid signature', async ({ mcp, auth }) => {
  // Create a token with an invalid signature
  const invalidToken = auth.createInvalidToken({
    sub: 'user-123',
  });

  await expect(mcp.authenticate(invalidToken))
    .rejects.toThrow('invalid signature');
});

Malformed Token

test('rejects malformed token', async ({ mcp }) => {
  await expect(mcp.authenticate('not.a.valid.token'))
    .rejects.toThrow();

  await expect(mcp.authenticate(''))
    .rejects.toThrow();
});

Testing Scope Enforcement

Scope-Based Access Control

test('enforces read-only scope', async ({ mcp, auth }) => {
  // Create read-only token
  const readOnlyToken = await auth.createToken({
    sub: 'user-123',
    scopes: ['read'],
  });

  await mcp.authenticate(readOnlyToken);

  // Read operations succeed
  const tools = await mcp.tools.list();
  expect(tools.length).toBeGreaterThan(0);

  // Write operations fail (assuming tool requires 'write' scope)
  const result = await mcp.tools.call('create-note', { title: 'Test' });
  expect(result).toBeError(); // Insufficient permissions
});

Testing Multiple Scopes

test('requires specific scopes', async ({ server, auth }) => {
  // Create clients with different scopes
  const readClient = await server.createClient({
    token: await auth.createToken({ sub: 'user-1', scopes: ['read'] }),
  });

  const writeClient = await server.createClient({
    token: await auth.createToken({ sub: 'user-2', scopes: ['write'] }),
  });

  const fullClient = await server.createClient({
    token: await auth.createToken({ sub: 'user-3', scopes: ['read', 'write'] }),
  });

  // Test each client's access
  expect(await readClient.resources.list()).toHaveLength(5);

  const readResult = await readClient.tools.call('create-note', { title: 'Test' });
  expect(readResult).toBeError();

  const writeResult = await writeClient.tools.call('create-note', { title: 'Test' });
  expect(writeResult).toBeSuccessful();

  // Cleanup
  await readClient.disconnect();
  await writeClient.disconnect();
  await fullClient.disconnect();
});

JWKS Integration

For servers that verify tokens against JWKS endpoints:
test('JWKS endpoint', async ({ auth }) => {
  // Get the public JWKS
  const jwks = await auth.getJwks();

  expect(jwks.keys).toHaveLength(1);
  expect(jwks.keys[0].kty).toBe('RSA');
  expect(jwks.keys[0].use).toBe('sig');
});

test('issuer and audience', async ({ auth }) => {
  const issuer = auth.getIssuer();
  const audience = auth.getAudience();

  expect(issuer).toContain('frontmcp-test');
  expect(audience).toBe('mcp-server');
});

Testing Auth Modes

Public Mode (No Auth)

test.describe('Public Mode', () => {
  test.use({
    server: './src/main.ts',
    auth: { mode: 'public' },
  });

  test('allows anonymous access', async ({ mcp }) => {
    expect(mcp.auth.isAnonymous).toBe(true);

    // All operations work without token
    const tools = await mcp.tools.list();
    expect(tools.length).toBeGreaterThan(0);
  });
});

Orchestrated Mode (Auth Required)

test.describe('Orchestrated Mode', () => {
  test.use({
    server: './src/main.ts',
    auth: { mode: 'orchestrated', type: 'local' },
  });

  test('requires authentication', async ({ mcp }) => {
    // Without token, requests fail
    await expect(mcp.tools.list())
      .rejects.toThrow('Unauthorized');
  });

  test('works with valid token', async ({ mcp, auth }) => {
    const token = await auth.createToken({ sub: 'user-123' });
    await mcp.authenticate(token);

    const tools = await mcp.tools.list();
    expect(tools.length).toBeGreaterThan(0);
  });
});

Real-World Examples

Testing User Isolation

test('users can only access their own data', async ({ server, auth }) => {
  const user1Client = await server.createClient({
    token: await auth.createToken({ sub: 'user-1' }),
  });

  const user2Client = await server.createClient({
    token: await auth.createToken({ sub: 'user-2' }),
  });

  // User 1 creates a note
  await user1Client.tools.call('create-note', {
    title: 'Private Note',
  });

  // User 2 cannot see it
  const user2Notes = await user2Client.resources.read('notes://all');
  expect(user2Notes.json().notes).toHaveLength(0);

  // User 1 can see it
  const user1Notes = await user1Client.resources.read('notes://all');
  expect(user1Notes.json().notes).toHaveLength(1);

  await user1Client.disconnect();
  await user2Client.disconnect();
});

Testing Admin Operations

test('admin can perform privileged operations', async ({ server, auth }) => {
  const userClient = await server.createClient({
    token: await auth.createToken(auth.users.user),
  });

  const adminClient = await server.createClient({
    token: await auth.createToken(auth.users.admin),
  });

  // Regular user cannot delete all notes
  const userResult = await userClient.tools.call('admin:clear-all', {});
  expect(userResult).toBeError();

  // Admin can delete all notes
  const adminResult = await adminClient.tools.call('admin:clear-all', {});
  expect(adminResult).toBeSuccessful();

  await userClient.disconnect();
  await adminClient.disconnect();
});

Best Practices

Do:
  • Test both positive (valid token) and negative (invalid/expired) cases
  • Test scope enforcement for all protected operations
  • Use pre-built test users for common scenarios
  • Clean up created clients after multi-user tests
Don’t:
  • Hard-code tokens in tests (always use auth.createToken())
  • Skip expiration testing
  • Assume scopes are enforced without testing
  • Forget to test anonymous access for public endpoints

MockOAuthServer

For integration testing of OAuth flows, use MockOAuthServer from @frontmcp/testing. It provides a fully functional OAuth 2.1 server with PKCE support.

Basic Usage

import { MockOAuthServer, TestTokenFactory } from '@frontmcp/testing';

// Create a token factory (shares signing keys with server)
const tokenFactory = new TestTokenFactory();
const oauthServer = new MockOAuthServer(tokenFactory, {
  autoApprove: true,
  testUser: { sub: 'user-123', email: 'test@example.com' },
  clientId: 'my-client',
  validRedirectUris: ['http://localhost:3000/callback'],
});

// Start the server
const serverInfo = await oauthServer.start();
console.log(`OAuth server running at ${serverInfo.baseUrl}`);

// Use serverInfo.issuer for your MCP server configuration

// Stop when done
await oauthServer.stop();

MockOAuthServerOptions

OptionTypeDefaultDescription
portnumberRandomPort to listen on
issuerstringAutoIssuer URL for tokens
debugbooleanfalseEnable debug logging
autoApprovebooleanfalseAuto-approve authorization requests
testUserMockTestUser-User to return on authorization
clientIdstring-Expected client ID for validation
clientSecretstring-Client secret (for confidential clients)
validRedirectUrisstring[]-Allowed redirect URIs (supports wildcards)
accessTokenTtlSecondsnumber3600Access token lifetime
refreshTokenTtlSecondsnumber2592000Refresh token lifetime (30 days)

MockTestUser Interface

interface MockTestUser {
  sub: string;                       // Subject identifier (required)
  email?: string;                    // User email
  name?: string;                     // Display name
  picture?: string;                  // Profile picture URL
  claims?: Record<string, unknown>;  // Additional token claims
}

MockOAuthServerInfo Interface

interface MockOAuthServerInfo {
  baseUrl: string;   // e.g., http://localhost:54321
  port: number;      // e.g., 54321
  issuer: string;    // e.g., http://localhost:54321
  jwksUrl: string;   // e.g., http://localhost:54321/.well-known/jwks.json
}

Available Endpoints

EndpointMethodDescription
/.well-known/jwks.jsonGETPublic signing keys
/.well-known/openid-configurationGETOIDC discovery document
/.well-known/oauth-authorization-serverGETOAuth metadata
/oauth/authorizeGETAuthorization endpoint
/oauth/authorize/submitPOSTAuthorization form submission
/oauth/tokenPOSTToken endpoint
/userinfoGETUser info endpoint

Supported Grant Types

  • authorization_code - Standard OAuth flow with PKCE
  • refresh_token - Refresh token rotation
  • anonymous - Issue anonymous tokens for testing

Example: Full OAuth Flow Test

import { test, expect } from '@playwright/test';
import { MockOAuthServer, TestTokenFactory } from '@frontmcp/testing';

test('complete OAuth authorization flow', async ({ page }) => {
  const tokenFactory = new TestTokenFactory();
  const oauthServer = new MockOAuthServer(tokenFactory, {
    autoApprove: true,
    testUser: {
      sub: 'test-user-123',
      email: 'test@example.com',
      name: 'Test User',
    },
    clientId: 'test-client',
    validRedirectUris: ['http://localhost:3000/*'],
  });

  const { baseUrl } = await oauthServer.start();

  try {
    // Configure your app to use this OAuth server
    // Then navigate to your app's login
    await page.goto('http://localhost:3000/login');

    // With autoApprove=true, the server automatically redirects
    // back with an authorization code

    // Verify the callback was successful
    await expect(page).toHaveURL(/callback.*code=/);
  } finally {
    await oauthServer.stop();
  }
});

Configuration Methods

// Change auto-approve mode at runtime
oauthServer.setAutoApprove(true);

// Update test user
oauthServer.setTestUser({ sub: 'new-user', email: 'new@example.com' });

// Add valid redirect URI
oauthServer.addValidRedirectUri('http://localhost:4000/callback');

// Clear stored tokens (for test isolation)
oauthServer.clearStoredTokens();

// Access the token factory
const factory = oauthServer.getTokenFactory();
MockOAuthServer is intended for testing only. It accepts any valid-looking credentials in non-autoApprove mode. Never use it in production.