Skip to content

Commit e705069

Browse files
authored
chore(repo): Add oauth_token type integration test (#7323)
1 parent 3f99742 commit e705069

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

.changeset/warm-phones-compete.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { randomBytes } from 'node:crypto';
2+
3+
import type { OAuthApplication } from '@clerk/backend';
4+
import { createClerkClient } from '@clerk/backend';
5+
import { TokenType } from '@clerk/backend/internal';
6+
import { expect, test } from '@playwright/test';
7+
8+
import type { Application } from '../../models/application';
9+
import { appConfigs } from '../../presets';
10+
import type { FakeUser } from '../../testUtils';
11+
import { createTestUtils } from '../../testUtils';
12+
13+
test.describe('OAuth machine authentication @machine', () => {
14+
test.describe.configure({ mode: 'parallel' });
15+
let app: Application;
16+
let fakeUser: FakeUser;
17+
let oAuthApp: OAuthApplication;
18+
19+
test.beforeAll(async () => {
20+
test.setTimeout(120_000);
21+
22+
app = await appConfigs.next.appRouter
23+
.clone()
24+
.addFile(
25+
'src/app/api/protected/route.ts',
26+
() => `
27+
import { auth } from '@clerk/nextjs/server';
28+
29+
export async function GET() {
30+
const { userId, tokenType } = await auth({ acceptsToken: 'oauth_token' });
31+
32+
if (!userId) {
33+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
34+
}
35+
36+
return Response.json({ userId, tokenType });
37+
}
38+
`,
39+
)
40+
.addFile(
41+
'src/app/oauth/callback/route.ts',
42+
() => `
43+
import { NextResponse } from 'next/server';
44+
45+
export async function GET() {
46+
return NextResponse.json({ message: 'OAuth callback received' });
47+
}
48+
`,
49+
)
50+
.commit();
51+
52+
await app.setup();
53+
await app.withEnv(appConfigs.envs.withEmailCodes);
54+
await app.dev();
55+
56+
// Test user that will authorize the OAuth application
57+
const u = createTestUtils({ app });
58+
fakeUser = u.services.users.createFakeUser();
59+
await u.services.users.createBapiUser(fakeUser);
60+
61+
const clerkClient = createClerkClient({
62+
secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'),
63+
publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'),
64+
});
65+
66+
// Create an OAuth application via the BAPI
67+
oAuthApp = await clerkClient.oauthApplications.create({
68+
name: `Integration Test OAuth App - ${Date.now()}`,
69+
redirectUris: [`${app.serverUrl}/oauth/callback`],
70+
scopes: 'profile email',
71+
});
72+
});
73+
74+
test.afterAll(async () => {
75+
const clerkClient = createClerkClient({
76+
secretKey: app.env.privateVariables.get('CLERK_SECRET_KEY'),
77+
publishableKey: app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'),
78+
});
79+
80+
if (oAuthApp.id) {
81+
await clerkClient.oauthApplications.delete(oAuthApp.id);
82+
}
83+
84+
await fakeUser.deleteIfExists();
85+
await app.teardown();
86+
});
87+
88+
test('verifies valid OAuth access token obtained through authorization flow', async ({ page, context }) => {
89+
const u = createTestUtils({ app, page, context });
90+
91+
// Build the authorization URL
92+
const state = randomBytes(16).toString('hex');
93+
const redirectUri = `${app.serverUrl}/oauth/callback`;
94+
const authorizeUrl = new URL(oAuthApp.authorizeUrl);
95+
authorizeUrl.searchParams.set('client_id', oAuthApp.clientId);
96+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
97+
authorizeUrl.searchParams.set('response_type', 'code');
98+
authorizeUrl.searchParams.set('scope', 'profile email');
99+
authorizeUrl.searchParams.set('state', state);
100+
101+
// Navigate to Clerk's authorization endpoint
102+
await u.page.goto(authorizeUrl.toString());
103+
104+
// Sign in on Account Portal
105+
await u.po.signIn.waitForMounted();
106+
await u.po.signIn.signInWithEmailAndInstantPassword({
107+
email: fakeUser.email,
108+
password: fakeUser.password,
109+
});
110+
111+
// Accept consent screen
112+
// Per https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth#consent-screen-management
113+
const consentButton = u.page.getByRole('button', { name: 'Allow' });
114+
await consentButton.waitFor({ timeout: 10000 });
115+
await consentButton.click();
116+
117+
// Wait for the redirect to complete
118+
await u.page.waitForURL(/oauth\/callback/, { timeout: 10000 });
119+
120+
// Extract the authorization code from the callback URL
121+
const currentUrl = u.page.url();
122+
const urlObj = new URL(currentUrl);
123+
const finalAuthCode = urlObj.searchParams.get('code');
124+
125+
expect(finalAuthCode).toBeTruthy();
126+
127+
// Exchange authorization code for access token
128+
expect(oAuthApp.clientSecret).toBeTruthy();
129+
130+
const tokenResponse = await u.page.request.post(oAuthApp.tokenFetchUrl, {
131+
data: new URLSearchParams({
132+
grant_type: 'authorization_code',
133+
code: finalAuthCode,
134+
redirect_uri: redirectUri,
135+
client_id: oAuthApp.clientId,
136+
client_secret: oAuthApp.clientSecret,
137+
}).toString(),
138+
headers: {
139+
'Content-Type': 'application/x-www-form-urlencoded',
140+
},
141+
});
142+
143+
expect(tokenResponse.status()).toBe(200);
144+
const tokenResponseBody = await tokenResponse.text();
145+
146+
const tokenData = JSON.parse(tokenResponseBody) as { access_token?: string };
147+
const accessToken = tokenData.access_token;
148+
149+
expect(accessToken).toBeTruthy();
150+
151+
// Use the access token to authenticate a request to our protected route
152+
const protectedRouteUrl = new URL('/api/protected', app.serverUrl);
153+
const protectedResponse = await u.page.request.get(protectedRouteUrl.toString(), {
154+
headers: {
155+
Authorization: `Bearer ${accessToken}`,
156+
},
157+
});
158+
159+
expect(protectedResponse.status()).toBe(200);
160+
const authData = await protectedResponse.json();
161+
expect(authData.userId).toBeDefined();
162+
expect(authData.tokenType).toBe(TokenType.OAuthToken);
163+
});
164+
165+
test('rejects request without OAuth token', async ({ request }) => {
166+
const url = new URL('/api/protected', app.serverUrl);
167+
const res = await request.get(url.toString());
168+
expect(res.status()).toBe(401);
169+
});
170+
171+
test('rejects request with invalid OAuth token', async ({ request }) => {
172+
const url = new URL('/api/protected', app.serverUrl);
173+
const res = await request.get(url.toString(), {
174+
headers: {
175+
Authorization: 'Bearer invalid_oauth_token',
176+
},
177+
});
178+
expect(res.status()).toBe(401);
179+
});
180+
});

0 commit comments

Comments
 (0)