Skip to content

Commit 5072f14

Browse files
committed
OAuth token tests
1 parent 29c4888 commit 5072f14

File tree

4 files changed

+144
-5
lines changed

4 files changed

+144
-5
lines changed

apps/backend/src/oauth/model.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,19 @@ export class OAuthModel implements AuthorizationCodeModel {
104104
token.client = client;
105105
token.user = user;
106106
return {
107-
...token,
107+
accessToken: token.accessToken,
108+
accessTokenExpiresAt: token.accessTokenExpiresAt,
109+
refreshToken: token.refreshToken,
110+
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
111+
scope: token.scope,
112+
client: token.client,
113+
user: token.user,
114+
115+
// TODO remove deprecated camelCase properties
108116
newUser: user.newUser,
117+
is_new_user: user.newUser,
109118
afterCallbackRedirectUrl: user.afterCallbackRedirectUrl,
119+
after_callback_redirect_url: user.afterCallbackRedirectUrl,
110120
};
111121
}
112122

apps/e2e/tests/backend/backend-helpers.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
22
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
33
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
4+
import { camelCaseToSnakeCase } from "@stackframe/stack-shared/dist/utils/strings";
45
import { expect } from "vitest";
56
import { Context, Mailbox, NiceRequestInit, NiceResponse, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_ID, STACK_INTERNAL_PROJECT_SERVER_KEY, createMailbox, localRedirectUrl, niceFetch, updateCookiesFromResponse } from "../helpers";
67

@@ -52,7 +53,7 @@ function expectSnakeCase(obj: unknown, path: string): void {
5253
}
5354
} else {
5455
for (const [key, value] of Object.entries(obj)) {
55-
if (key.match(/[a-z0-9][A-Z][a-z0-9]+/) && !key.includes("_")) {
56+
if (key.match(/[a-z0-9][A-Z][a-z0-9]+/) && !key.includes("_") && !["newUser", "afterCallbackRedirectUrl"].includes(key)) {
5657
throw new StackAssertionError(`Object has camelCase key (expected snake case): ${path}.${key}`);
5758
}
5859
expectSnakeCase(value, `${path}.${key}`);
@@ -438,6 +439,58 @@ export namespace Auth {
438439
authorizationCode: outerCallbackUrl.searchParams.get("code")!,
439440
};
440441
}
442+
443+
export async function signIn() {
444+
const getAuthorizationCodeResult = await Auth.OAuth.getAuthorizationCode();
445+
446+
const projectKeys = backendContext.value.projectKeys;
447+
if (projectKeys === "no-project") throw new Error("No project keys found in the backend context");
448+
449+
const tokenResponse = await niceBackendFetch("/api/v1/auth/oauth/token", {
450+
method: "POST",
451+
accessType: "client",
452+
body: {
453+
client_id: projectKeys.projectId,
454+
client_secret: projectKeys.publishableClientKey ?? throwErr("No publishable client key found in the backend context"),
455+
code: getAuthorizationCodeResult.authorizationCode,
456+
redirect_uri: localRedirectUrl,
457+
code_verifier: "some-code-challenge",
458+
grant_type: "authorization_code",
459+
},
460+
});
461+
expect(tokenResponse).toMatchInlineSnapshot(`
462+
NiceResponse {
463+
"status": 200,
464+
"body": {
465+
"access_token": <stripped field 'access_token'>,
466+
"afterCallbackRedirectUrl": null,
467+
"after_callback_redirect_url": null,
468+
"expires_in": 3599,
469+
"is_new_user": true,
470+
"newUser": true,
471+
"refresh_token": <stripped field 'refresh_token'>,
472+
"scope": "legacy",
473+
"token_type": "Bearer",
474+
},
475+
"headers": Headers {
476+
"pragma": "no-cache",
477+
<some fields may have been hidden>,
478+
},
479+
}
480+
`);
481+
482+
backendContext.set({
483+
userAuth: {
484+
accessToken: tokenResponse.body.access_token,
485+
refreshToken: tokenResponse.body.refresh_token,
486+
},
487+
});
488+
489+
return {
490+
...getAuthorizationCodeResult,
491+
tokenResponse,
492+
};
493+
}
441494
}
442495
}
443496

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
import { describe } from "vitest";
3+
import { it } from "../../../../../../helpers";
4+
import { Auth, niceBackendFetch } from "../../../../../backend-helpers";
5+
6+
describe("with grant_type === 'authorization_code'", async () => {
7+
it("should sign in a user when called as part of the OAuth flow", async ({ expect }) => {
8+
const response = await Auth.OAuth.signIn();
9+
expect(response.tokenResponse).toMatchInlineSnapshot(`
10+
NiceResponse {
11+
"status": 200,
12+
"body": {
13+
"access_token": <stripped field 'access_token'>,
14+
"afterCallbackRedirectUrl": null,
15+
"after_callback_redirect_url": null,
16+
"expires_in": 3599,
17+
"is_new_user": true,
18+
"newUser": true,
19+
"refresh_token": <stripped field 'refresh_token'>,
20+
"scope": "legacy",
21+
"token_type": "Bearer",
22+
},
23+
"headers": Headers {
24+
"pragma": "no-cache",
25+
<some fields may have been hidden>,
26+
},
27+
}
28+
`);
29+
await Auth.expectToBeSignedIn();
30+
const meResponse = await niceBackendFetch("/api/v1/users/me", { accessType: "client" });
31+
expect(meResponse).toMatchInlineSnapshot(`
32+
NiceResponse {
33+
"status": 200,
34+
"body": {
35+
"auth_methods": [
36+
{
37+
"provider": {
38+
"provider_user_id": "<stripped UUID>@stack-generated.example.com",
39+
"type": "facebook",
40+
},
41+
"type": "oauth",
42+
},
43+
],
44+
"auth_with_email": false,
45+
"client_metadata": null,
46+
"connected_accounts": [
47+
{
48+
"provider": {
49+
"provider_user_id": "<stripped UUID>@stack-generated.example.com",
50+
"type": "facebook",
51+
},
52+
"type": "oauth",
53+
},
54+
],
55+
"display_name": null,
56+
"has_password": false,
57+
"id": "<stripped UUID>",
58+
"oauth_providers": [
59+
{
60+
"account_id": "<stripped UUID>@stack-generated.example.com",
61+
"email": "<stripped UUID>@stack-generated.example.com",
62+
"id": "facebook",
63+
},
64+
],
65+
"primary_email": "<stripped UUID>@stack-generated.example.com",
66+
"primary_email_verified": false,
67+
"profile_image_url": null,
68+
"selected_team": null,
69+
"selected_team_id": null,
70+
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
71+
},
72+
"headers": Headers { <some fields may have been hidden> },
73+
}
74+
`);
75+
});
76+
});

packages/stack-shared/src/interface/clientInterface.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ export class StackClientInterface {
685685
return {
686686
accessToken: result.access_token,
687687
refreshToken: result.refresh_token,
688-
newUser: result.new_user,
688+
newUser: result.is_new_user,
689689
};
690690
}
691691

@@ -782,8 +782,8 @@ export class StackClientInterface {
782782
throw new StackAssertionError("Outer OAuth error during authorization code response", { result });
783783
}
784784
return {
785-
newUser: result.newUser as boolean,
786-
afterCallbackRedirectUrl: result.afterCallbackRedirectUrl as string | undefined,
785+
newUser: result.is_new_user as boolean,
786+
afterCallbackRedirectUrl: result.after_callback_redirect_url as string | undefined,
787787
accessToken: result.access_token,
788788
refreshToken: result.refresh_token ?? throwErr("Refresh token not found in outer OAuth response"),
789789
};

0 commit comments

Comments
 (0)