Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,36 @@ new OAuthProvider({

By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.

## Passing custom or per-request props

If your application needs to pass through context from each request to the ApiHandler, you can pass a `customProps` callback. This receives the same parameters (`request`, `env`, and `ctx`) as a standard Workers event handler, and lets you return arbitrary data that will be forwarded to your handler inside `ctx.props.customProps`.

```ts
new OAuthProvider({
// ... other options ...
customProps: async (request, env, ctx) => {
// Will be added to ctx.props.customProps when invoking apiHandler
return {
customToken: request.headers.get('x-custom-token'),
featureFlags: await env.FEATURES.parse(request.headers.get('x-feature-flags')),
}
}
})
```

For use with Cloudflare's [`McpAgent`](https://developers.cloudflare.com/agents/model-context-protocol/mcp-agent-api/), this will be automatically available on `this.props.customProps`:

```ts
export class MyMCP extends McpAgent {
server = new McpServer({ /* ... */ });

async init() {
const { customToken, featureFlags } = this.props.customProps
// ...
}
}
```

## Implementation Notes

### End-to-end encryption
Expand Down
121 changes: 119 additions & 2 deletions __tests__/oauth-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { OAuthProvider, ClientInfo, AuthRequest, CompleteAuthorizationOptions } from '../src/oauth-provider';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { OAuthProvider } from '../src/oauth-provider';
import { ExecutionContext } from '@cloudflare/workers-types';
// We're importing WorkerEntrypoint from our mock implementation
// The actual import is mocked in setup.ts
Expand Down Expand Up @@ -2230,4 +2230,121 @@ describe('OAuthProvider', () => {
expect(clientsAfterDelete.items.length).toBe(0);
});
});

describe('Custom Props', () => {
it('should call customProps callback and include result in ctx.props.customProps', async () => {
const oauthProviderWithCustomProps = new OAuthProvider({
apiRoute: ['/api/', 'https://api.example.com/'],
apiHandler: TestApiHandler,
defaultHandler: testDefaultHandler,
authorizeEndpoint: '/authorize',
tokenEndpoint: '/oauth/token',
clientRegistrationEndpoint: '/oauth/register',
customProps: async (request: Request, env: any, ctx: ExecutionContext) => {
// Return custom props based on request headers
return {
customToken: request.headers.get('x-custom-token'),
requestId: request.headers.get('x-request-id'),
userAgent: request.headers.get('user-agent'),
};
},
});

// Create a client
const clientData = {
redirect_uris: ['https://client.example.com/callback'],
client_name: 'Test Client',
token_endpoint_auth_method: 'client_secret_basic',
};

const registerRequest = createMockRequest(
'https://example.com/oauth/register',
'POST',
{ 'Content-Type': 'application/json' },
JSON.stringify(clientData)
);

const registerResponse = await oauthProviderWithCustomProps.fetch(registerRequest, mockEnv, mockCtx);
const client = await registerResponse.json();
const clientId = client.client_id;
const clientSecret = client.client_secret;

// Get an auth code
const redirectUri = 'https://client.example.com/callback';
const authRequest = createMockRequest(
`https://example.com/authorize?response_type=code&client_id=${clientId}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&scope=read%20write&state=xyz123`
);

const authResponse = await oauthProviderWithCustomProps.fetch(authRequest, mockEnv, mockCtx);
const location = authResponse.headers.get('Location')!;
const code = new URL(location).searchParams.get('code')!;

// Exchange for tokens
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', redirectUri);
params.append('client_id', clientId);
params.append('client_secret', clientSecret);

const tokenRequest = createMockRequest(
'https://example.com/oauth/token',
'POST',
{ 'Content-Type': 'application/x-www-form-urlencoded' },
params.toString()
);

const tokenResponse = await oauthProviderWithCustomProps.fetch(tokenRequest, mockEnv, mockCtx);
const tokens = await tokenResponse.json();
const accessToken = tokens.access_token;

{
// Make an API request with custom headers
const apiRequest = createMockRequest('https://example.com/api/test', 'GET', {
Authorization: `Bearer ${accessToken}`,
'x-custom-token': 'custom-token-123',
'x-request-id': 'req-456',
'user-agent': 'Test User Agent',
});

const apiResponse = await oauthProviderWithCustomProps.fetch(apiRequest, mockEnv, mockCtx);

expect(apiResponse.status).toBe(200);

const result = await apiResponse.json();
expect(result.success).toBe(true);

// Verify the custom props were added to ctx.props.customProps
expect(result.user.customProps).toBeDefined();
expect(result.user.customProps.customToken).toBe('custom-token-123');
expect(result.user.customProps.requestId).toBe('req-456');
expect(result.user.customProps.userAgent).toBe('Test User Agent');
}

{
// Make an API request with different custom headers
const apiRequest = createMockRequest('https://example.com/api/test', 'GET', {
Authorization: `Bearer ${accessToken}`,
'x-custom-token': 'custom-token-456',
'x-request-id': 'req-789',
'user-agent': 'Test User Agent 2.0',
});

const apiResponse = await oauthProviderWithCustomProps.fetch(apiRequest, mockEnv, mockCtx);

expect(apiResponse.status).toBe(200);

const result = await apiResponse.json();
expect(result.success).toBe(true);

// Verify the custom props were added to ctx.props.customProps
expect(result.user.customProps).toBeDefined();
expect(result.user.customProps.customToken).toBe('custom-token-456');
expect(result.user.customProps.requestId).toBe('req-789');
expect(result.user.customProps.userAgent).toBe('Test User Agent 2.0');
}
});
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
"tsup": "^8.4.0",
"typescript": "^5.8.2",
"vitest": "^3.0.8"
}
},
"packageManager": "[email protected]+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
12 changes: 12 additions & 0 deletions src/oauth-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ export interface OAuthProviderOptions {
status: number;
headers: Record<string, string>;
}) => Response | void;

/**
* Optional callback function that is called for each API request to provide custom props.
* This receives the same parameters (request, env, ctx) as a standard Workers event handler,
* and lets you return arbitrary data that will be forwarded to your handler inside ctx.props.customProps.
*/
customProps?: (request: Request, env: any, ctx: ExecutionContext) => Promise<any> | any;
}

// Using ExportedHandler from Cloudflare Workers Types for both API and default handlers
Expand Down Expand Up @@ -1835,6 +1842,11 @@ class OAuthProviderImpl {
// Set the decrypted props on the context object
ctx.props = decryptedProps;

// Add custom props if callback is provided
if (this.options.customProps) {
ctx.props.customProps = await this.options.customProps(request, env, ctx);
}

// Inject OAuth helpers into env if not already present
if (!env.OAUTH_PROVIDER) {
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
Expand Down
Loading