> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication Testing

> Test authentication flows with JWT token generation

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:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

| Option | Type | Required | Description |
| - | - | - | - |
| `sub` | `string` | Yes | Subject (user identifier) |
| `scopes` | `string[]` | No | OAuth scopes |
| `email` | `string` | No | Email claim |
| `name` | `string` | No | Name claim |
| `claims` | `Record<string, unknown>` | No | Additional custom claims |
| `expiresIn` | `number` | No | Token lifetime in seconds (default: 3600) |

The default issuer is `https://test.frontmcp.local` and the default audience is `frontmcp-test`. Override them with `new TestTokenFactory({ issuer, audience })`.

`expiresIn` is measured from now and rounded up to a whole second, so `expiresIn: 1` gives a token that is valid for at least one full second and cannot expire before its first use.

### Authenticating the client

`mcp.authenticate(token)` opens a new session for the token and **rejects** when the server refuses it, so an expired or badly signed token fails right there. After a rejection the client keeps its previous identity and session. On a client that is not connected yet it only stores the token for the next `connect()`.

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
test('server refuses a bad token', async ({ mcp, auth }) => {
  const good = await auth.createToken({ sub: 'user-1' });
  await mcp.authenticate(good);

  await expect(mcp.authenticate(auth.createInvalidToken({ sub: 'user-1' }))).rejects.toThrow();
  // still authenticated as user-1
  await mcp.tools.list();
});
```

Tools read the scopes of the authenticated token through `this.auth` (for example `this.auth.scopes`), including in a server started by `test.use()`.

### Custom Claims

Add any custom claims your server needs:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

| User | `sub` | Scopes |
| - | - | - |
| `admin` | `admin-001` | `['admin:*', 'read', 'write', 'delete']` |
| `user` | `user-001` | `['read', 'write']` |
| `readOnly` | `readonly-001` | `['read']` |

***

## Testing Token Expiration

### Expired Tokens

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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);
  });
});
```

### Local Mode (Auth Required)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
test.describe('Local Mode', () => {
  test.use({
    server: './src/main.ts',
    auth: { mode: '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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

| Option | Type | Default | Description |
| - | - | - | - |
| `port` | `number` | Random | Port to listen on |
| `issuer` | `string` | Auto | Issuer URL for tokens |
| `debug` | `boolean` | `false` | Enable debug logging |
| `autoApprove` | `boolean` | `false` | Auto-approve authorization requests |
| `testUser` | `MockTestUser` | - | User to return on authorization |
| `clientId` | `string` | - | Expected client ID for validation |
| `clientSecret` | `string` | - | Client secret (for confidential clients) |
| `validRedirectUris` | `string[]` | - | Allowed redirect URIs (supports wildcards) |
| `accessTokenTtlSeconds` | `number` | `3600` | Access token lifetime |
| `refreshTokenTtlSeconds` | `number` | `2592000` | Refresh token lifetime (30 days) |

### MockTestUser Interface

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

| Endpoint | Method | Description |
| - | - | - |
| `/.well-known/jwks.json` | GET | Public signing keys |
| `/.well-known/openid-configuration` | GET | OIDC discovery document |
| `/.well-known/oauth-authorization-server` | GET | OAuth metadata |
| `/oauth/authorize` | GET | Authorization endpoint |
| `/oauth/authorize/submit` | POST | Authorization form submission |
| `/oauth/token` | POST | Token endpoint |
| `/userinfo` | GET | User 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

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// 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();
```

<Warning>
  MockOAuthServer is intended for testing only. It accepts any valid-looking credentials in non-autoApprove mode. Never use it in production.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.