Skip to content

Commit 87f090d

Browse files
committed
Ask Claude to fully implement the implicit flow.
prompt: Earlier we added the `allowImplicitFlow` option but I don't think it was actually fully implemented. Can you check? Claude correctly reported that it wasn't properly implemented. prompt: Yes, let's implement it. Claude implemented it, and added a test. Claude tried to run the test, but "npm command not found", I guess its PATH isn't inherited from mine for some reason...
1 parent d92d8b0 commit 87f090d

File tree

2 files changed

+315
-40
lines changed

2 files changed

+315
-40
lines changed

__tests__/oauth-provider.test.ts

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ describe('OAuthProvider', () => {
241241
tokenEndpoint: '/oauth/token',
242242
clientRegistrationEndpoint: '/oauth/register',
243243
scopesSupported: ['read', 'write', 'profile'],
244-
accessTokenTTL: 3600
244+
accessTokenTTL: 3600,
245+
allowImplicitFlow: true // Enable implicit flow for tests
245246
});
246247
});
247248

@@ -264,9 +265,32 @@ describe('OAuthProvider', () => {
264265
expect(metadata.registration_endpoint).toBe('https://example.com/oauth/register');
265266
expect(metadata.scopes_supported).toEqual(['read', 'write', 'profile']);
266267
expect(metadata.response_types_supported).toContain('code');
268+
expect(metadata.response_types_supported).toContain('token'); // Implicit flow enabled
267269
expect(metadata.grant_types_supported).toContain('authorization_code');
268270
expect(metadata.code_challenge_methods_supported).toContain('S256');
269271
});
272+
273+
it('should not include token response type when implicit flow is disabled', async () => {
274+
// Create a provider with implicit flow disabled
275+
const providerWithoutImplicit = new OAuthProvider({
276+
apiRoute: ['/api/'],
277+
apiHandler: TestApiHandler,
278+
defaultHandler: testDefaultHandler,
279+
authorizeEndpoint: '/authorize',
280+
tokenEndpoint: '/oauth/token',
281+
scopesSupported: ['read', 'write'],
282+
allowImplicitFlow: false // Explicitly disable
283+
});
284+
285+
const request = createMockRequest('https://example.com/.well-known/oauth-authorization-server');
286+
const response = await providerWithoutImplicit.fetch(request, mockEnv, mockCtx);
287+
288+
expect(response.status).toBe(200);
289+
290+
const metadata = await response.json();
291+
expect(metadata.response_types_supported).toContain('code');
292+
expect(metadata.response_types_supported).not.toContain('token');
293+
});
270294
});
271295

272296
describe('Client Registration', () => {
@@ -394,6 +418,180 @@ describe('OAuthProvider', () => {
394418
expect(grants.keys.length).toBe(1);
395419
});
396420

421+
// Add more tests for auth code flow...
422+
});
423+
424+
describe('Implicit Flow', () => {
425+
let clientId: string;
426+
let redirectUri: string;
427+
428+
// Helper to create a test client before authorization tests
429+
async function createPublicClient() {
430+
const clientData = {
431+
redirect_uris: ['https://spa-client.example.com/callback'],
432+
client_name: 'SPA Test Client',
433+
token_endpoint_auth_method: 'none' // Public client
434+
};
435+
436+
const request = createMockRequest(
437+
'https://example.com/oauth/register',
438+
'POST',
439+
{ 'Content-Type': 'application/json' },
440+
JSON.stringify(clientData)
441+
);
442+
443+
const response = await oauthProvider.fetch(request, mockEnv, mockCtx);
444+
const client = await response.json();
445+
446+
clientId = client.client_id;
447+
redirectUri = 'https://spa-client.example.com/callback';
448+
}
449+
450+
beforeEach(async () => {
451+
await createPublicClient();
452+
});
453+
454+
it('should handle implicit flow request and redirect with token in fragment', async () => {
455+
// Create an implicit flow authorization request
456+
const authRequest = createMockRequest(
457+
`https://example.com/authorize?response_type=token&client_id=${clientId}` +
458+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
459+
`&scope=read%20write&state=xyz123`
460+
);
461+
462+
// The default handler will process this request and generate a redirect
463+
const response = await oauthProvider.fetch(authRequest, mockEnv, mockCtx);
464+
465+
expect(response.status).toBe(302);
466+
467+
// Check that we're redirected to the client's redirect_uri with token in fragment
468+
const location = response.headers.get('Location');
469+
expect(location).toBeDefined();
470+
expect(location).toContain(redirectUri);
471+
472+
const url = new URL(location!);
473+
474+
// Check that there's no code parameter in the query string
475+
expect(url.searchParams.has('code')).toBe(false);
476+
477+
// Check that we have a hash/fragment with token parameters
478+
expect(url.hash).toBeTruthy();
479+
480+
// Parse the fragment
481+
const fragment = new URLSearchParams(url.hash.substring(1)); // Remove the # character
482+
483+
// Verify token parameters
484+
expect(fragment.get('access_token')).toBeTruthy();
485+
expect(fragment.get('token_type')).toBe('bearer');
486+
expect(fragment.get('expires_in')).toBe('3600');
487+
expect(fragment.get('scope')).toBe('read write');
488+
expect(fragment.get('state')).toBe('xyz123');
489+
490+
// Verify a grant was created in KV
491+
const grants = await mockEnv.OAUTH_KV.list({ prefix: 'grant:' });
492+
expect(grants.keys.length).toBe(1);
493+
494+
// Verify access token was stored in KV
495+
const tokenEntries = await mockEnv.OAUTH_KV.list({ prefix: 'token:' });
496+
expect(tokenEntries.keys.length).toBe(1);
497+
});
498+
499+
it('should reject implicit flow when allowImplicitFlow is disabled', async () => {
500+
// Create a provider with implicit flow disabled
501+
const providerWithoutImplicit = new OAuthProvider({
502+
apiRoute: ['/api/'],
503+
apiHandler: TestApiHandler,
504+
defaultHandler: testDefaultHandler,
505+
authorizeEndpoint: '/authorize',
506+
tokenEndpoint: '/oauth/token',
507+
scopesSupported: ['read', 'write'],
508+
allowImplicitFlow: false // Explicitly disable
509+
});
510+
511+
// Create an implicit flow authorization request
512+
const authRequest = createMockRequest(
513+
`https://example.com/authorize?response_type=token&client_id=${clientId}` +
514+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
515+
`&scope=read%20write&state=xyz123`
516+
);
517+
518+
// Mock parseAuthRequest to test error handling
519+
vi.spyOn(authRequest, 'formData').mockImplementation(() => {
520+
throw new Error('The implicit grant flow is not enabled for this provider');
521+
});
522+
523+
// Expect an error response
524+
await expect(providerWithoutImplicit.fetch(authRequest, mockEnv, mockCtx)).rejects.toThrow(
525+
'The implicit grant flow is not enabled for this provider'
526+
);
527+
});
528+
529+
it('should use the access token to access API directly', async () => {
530+
// Create an implicit flow authorization request
531+
const authRequest = createMockRequest(
532+
`https://example.com/authorize?response_type=token&client_id=${clientId}` +
533+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
534+
`&scope=read%20write&state=xyz123`
535+
);
536+
537+
// The default handler will process this request and generate a redirect
538+
const response = await oauthProvider.fetch(authRequest, mockEnv, mockCtx);
539+
const location = response.headers.get('Location')!;
540+
541+
// Parse the fragment to get the access token
542+
const url = new URL(location);
543+
const fragment = new URLSearchParams(url.hash.substring(1));
544+
const accessToken = fragment.get('access_token')!;
545+
546+
// Now use the access token for an API request
547+
const apiRequest = createMockRequest(
548+
'https://example.com/api/test',
549+
'GET',
550+
{ 'Authorization': `Bearer ${accessToken}` }
551+
);
552+
553+
const apiResponse = await oauthProvider.fetch(apiRequest, mockEnv, mockCtx);
554+
555+
expect(apiResponse.status).toBe(200);
556+
557+
const apiData = await apiResponse.json();
558+
expect(apiData.success).toBe(true);
559+
expect(apiData.user).toEqual({ userId: "test-user-123", username: "TestUser" });
560+
});
561+
});
562+
563+
describe('Authorization Code Flow Exchange', () => {
564+
let clientId: string;
565+
let clientSecret: string;
566+
let redirectUri: string;
567+
568+
// Helper to create a test client before authorization tests
569+
async function createTestClient() {
570+
const clientData = {
571+
redirect_uris: ['https://client.example.com/callback'],
572+
client_name: 'Test Client',
573+
token_endpoint_auth_method: 'client_secret_basic'
574+
};
575+
576+
const request = createMockRequest(
577+
'https://example.com/oauth/register',
578+
'POST',
579+
{ 'Content-Type': 'application/json' },
580+
JSON.stringify(clientData)
581+
);
582+
583+
const response = await oauthProvider.fetch(request, mockEnv, mockCtx);
584+
const client = await response.json();
585+
586+
clientId = client.client_id;
587+
clientSecret = client.client_secret;
588+
redirectUri = 'https://client.example.com/callback';
589+
}
590+
591+
beforeEach(async () => {
592+
await createTestClient();
593+
});
594+
397595
it('should exchange auth code for tokens', async () => {
398596
// First get an auth code
399597
const authRequest = createMockRequest(

0 commit comments

Comments
 (0)