@better-auth/core 1.5.0-beta.8 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/api/index.d.mts +144 -41
- package/dist/api/index.mjs +2 -1
- package/dist/api/index.mjs.map +1 -0
- package/dist/async_hooks/index.d.mts +2 -1
- package/dist/async_hooks/index.mjs +2 -1
- package/dist/async_hooks/index.mjs.map +1 -0
- package/dist/async_hooks/pure.index.d.mts +2 -1
- package/dist/async_hooks/pure.index.mjs +2 -1
- package/dist/async_hooks/pure.index.mjs.map +1 -0
- package/dist/context/endpoint-context.d.mts +2 -1
- package/dist/context/endpoint-context.mjs +4 -3
- package/dist/context/endpoint-context.mjs.map +1 -0
- package/dist/context/global.d.mts +2 -2
- package/dist/context/global.mjs +3 -2
- package/dist/context/global.mjs.map +1 -0
- package/dist/context/index.d.mts +2 -2
- package/dist/context/index.mjs +2 -2
- package/dist/context/request-state.d.mts +2 -1
- package/dist/context/request-state.mjs +4 -3
- package/dist/context/request-state.mjs.map +1 -0
- package/dist/context/transaction.d.mts +12 -3
- package/dist/context/transaction.mjs +55 -11
- package/dist/context/transaction.mjs.map +1 -0
- package/dist/db/adapter/factory.d.mts +6 -13
- package/dist/db/adapter/factory.mjs +44 -57
- package/dist/db/adapter/factory.mjs.map +1 -0
- package/dist/db/adapter/get-default-field-name.d.mts +2 -1
- package/dist/db/adapter/get-default-field-name.mjs +3 -2
- package/dist/db/adapter/get-default-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-default-model-name.d.mts +2 -1
- package/dist/db/adapter/get-default-model-name.mjs +5 -4
- package/dist/db/adapter/get-default-model-name.mjs.map +1 -0
- package/dist/db/adapter/get-field-attributes.d.mts +3 -2
- package/dist/db/adapter/get-field-attributes.mjs +2 -1
- package/dist/db/adapter/get-field-attributes.mjs.map +1 -0
- package/dist/db/adapter/get-field-name.d.mts +2 -1
- package/dist/db/adapter/get-field-name.mjs +2 -1
- package/dist/db/adapter/get-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-id-field.d.mts +3 -2
- package/dist/db/adapter/get-id-field.mjs +3 -2
- package/dist/db/adapter/get-id-field.mjs.map +1 -0
- package/dist/db/adapter/get-model-name.d.mts +2 -1
- package/dist/db/adapter/get-model-name.mjs +2 -1
- package/dist/db/adapter/get-model-name.mjs.map +1 -0
- package/dist/db/adapter/index.d.mts +10 -4
- package/dist/db/adapter/index.mjs +19 -2
- package/dist/db/adapter/index.mjs.map +1 -0
- package/dist/db/adapter/types.d.mts +3 -34
- package/dist/db/adapter/utils.d.mts +2 -1
- package/dist/db/adapter/utils.mjs +2 -1
- package/dist/db/adapter/utils.mjs.map +1 -0
- package/dist/db/get-tables.d.mts +2 -1
- package/dist/db/get-tables.mjs +46 -39
- package/dist/db/get-tables.mjs.map +1 -0
- package/dist/db/index.d.mts +7 -7
- package/dist/db/plugin.d.mts +2 -1
- package/dist/db/schema/account.d.mts +8 -4
- package/dist/db/schema/account.mjs +2 -1
- package/dist/db/schema/account.mjs.map +1 -0
- package/dist/db/schema/rate-limit.d.mts +8 -2
- package/dist/db/schema/rate-limit.mjs +2 -1
- package/dist/db/schema/rate-limit.mjs.map +1 -0
- package/dist/db/schema/session.d.mts +8 -4
- package/dist/db/schema/session.mjs +2 -1
- package/dist/db/schema/session.mjs.map +1 -0
- package/dist/db/schema/shared.d.mts +2 -1
- package/dist/db/schema/shared.mjs +2 -1
- package/dist/db/schema/shared.mjs.map +1 -0
- package/dist/db/schema/user.d.mts +8 -4
- package/dist/db/schema/user.mjs +2 -1
- package/dist/db/schema/user.mjs.map +1 -0
- package/dist/db/schema/verification.d.mts +8 -4
- package/dist/db/schema/verification.mjs +2 -1
- package/dist/db/schema/verification.mjs.map +1 -0
- package/dist/db/type.d.mts +28 -2
- package/dist/env/color-depth.d.mts +2 -1
- package/dist/env/color-depth.mjs +2 -1
- package/dist/env/color-depth.mjs.map +1 -0
- package/dist/env/env-impl.d.mts +3 -2
- package/dist/env/env-impl.mjs +9 -8
- package/dist/env/env-impl.mjs.map +1 -0
- package/dist/env/logger.d.mts +2 -1
- package/dist/env/logger.mjs +3 -2
- package/dist/env/logger.mjs.map +1 -0
- package/dist/error/codes.d.mts +64 -181
- package/dist/error/codes.mjs +6 -2
- package/dist/error/codes.mjs.map +1 -0
- package/dist/error/index.d.mts +2 -1
- package/dist/error/index.mjs +2 -1
- package/dist/error/index.mjs.map +1 -0
- package/dist/index.d.mts +5 -4
- package/dist/oauth2/client-credentials-token.d.mts +25 -3
- package/dist/oauth2/client-credentials-token.mjs +15 -2
- package/dist/oauth2/client-credentials-token.mjs.map +1 -0
- package/dist/oauth2/create-authorization-url.d.mts +5 -2
- package/dist/oauth2/create-authorization-url.mjs +3 -1
- package/dist/oauth2/create-authorization-url.mjs.map +1 -0
- package/dist/oauth2/index.d.mts +4 -4
- package/dist/oauth2/index.mjs +4 -4
- package/dist/oauth2/oauth-provider.d.mts +3 -2
- package/dist/oauth2/refresh-access-token.d.mts +24 -4
- package/dist/oauth2/refresh-access-token.mjs +20 -2
- package/dist/oauth2/refresh-access-token.mjs.map +1 -0
- package/dist/oauth2/utils.d.mts +2 -1
- package/dist/oauth2/utils.mjs +2 -1
- package/dist/oauth2/utils.mjs.map +1 -0
- package/dist/oauth2/validate-authorization-code.d.mts +37 -4
- package/dist/oauth2/validate-authorization-code.mjs +25 -13
- package/dist/oauth2/validate-authorization-code.mjs.map +1 -0
- package/dist/oauth2/verify.d.mts +7 -13
- package/dist/oauth2/verify.mjs +2 -1
- package/dist/oauth2/verify.mjs.map +1 -0
- package/dist/social-providers/apple.d.mts +2 -1
- package/dist/social-providers/apple.mjs +22 -21
- package/dist/social-providers/apple.mjs.map +1 -0
- package/dist/social-providers/atlassian.d.mts +2 -1
- package/dist/social-providers/atlassian.mjs +2 -1
- package/dist/social-providers/atlassian.mjs.map +1 -0
- package/dist/social-providers/cognito.d.mts +2 -1
- package/dist/social-providers/cognito.mjs +4 -3
- package/dist/social-providers/cognito.mjs.map +1 -0
- package/dist/social-providers/discord.d.mts +2 -1
- package/dist/social-providers/discord.mjs +2 -1
- package/dist/social-providers/discord.mjs.map +1 -0
- package/dist/social-providers/dropbox.d.mts +2 -1
- package/dist/social-providers/dropbox.mjs +3 -2
- package/dist/social-providers/dropbox.mjs.map +1 -0
- package/dist/social-providers/facebook.d.mts +2 -1
- package/dist/social-providers/facebook.mjs +13 -12
- package/dist/social-providers/facebook.mjs.map +1 -0
- package/dist/social-providers/figma.d.mts +2 -1
- package/dist/social-providers/figma.mjs +2 -1
- package/dist/social-providers/figma.mjs.map +1 -0
- package/dist/social-providers/github.d.mts +3 -2
- package/dist/social-providers/github.mjs +23 -6
- package/dist/social-providers/github.mjs.map +1 -0
- package/dist/social-providers/gitlab.d.mts +2 -1
- package/dist/social-providers/gitlab.mjs +3 -2
- package/dist/social-providers/gitlab.mjs.map +1 -0
- package/dist/social-providers/google.d.mts +2 -1
- package/dist/social-providers/google.mjs +18 -13
- package/dist/social-providers/google.mjs.map +1 -0
- package/dist/social-providers/huggingface.d.mts +2 -1
- package/dist/social-providers/huggingface.mjs +3 -2
- package/dist/social-providers/huggingface.mjs.map +1 -0
- package/dist/social-providers/index.d.mts +61 -8
- package/dist/social-providers/index.mjs +5 -2
- package/dist/social-providers/index.mjs.map +1 -0
- package/dist/social-providers/kakao.d.mts +3 -2
- package/dist/social-providers/kakao.mjs +3 -2
- package/dist/social-providers/kakao.mjs.map +1 -0
- package/dist/social-providers/kick.d.mts +2 -1
- package/dist/social-providers/kick.mjs +2 -1
- package/dist/social-providers/kick.mjs.map +1 -0
- package/dist/social-providers/line.d.mts +2 -1
- package/dist/social-providers/line.mjs +3 -2
- package/dist/social-providers/line.mjs.map +1 -0
- package/dist/social-providers/linear.d.mts +2 -1
- package/dist/social-providers/linear.mjs +2 -1
- package/dist/social-providers/linear.mjs.map +1 -0
- package/dist/social-providers/linkedin.d.mts +2 -1
- package/dist/social-providers/linkedin.mjs +2 -1
- package/dist/social-providers/linkedin.mjs.map +1 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +4 -1
- package/dist/social-providers/microsoft-entra-id.mjs +36 -2
- package/dist/social-providers/microsoft-entra-id.mjs.map +1 -0
- package/dist/social-providers/naver.d.mts +11 -20
- package/dist/social-providers/naver.mjs +3 -2
- package/dist/social-providers/naver.mjs.map +1 -0
- package/dist/social-providers/notion.d.mts +2 -1
- package/dist/social-providers/notion.mjs +3 -2
- package/dist/social-providers/notion.mjs.map +1 -0
- package/dist/social-providers/paybin.d.mts +2 -1
- package/dist/social-providers/paybin.mjs +3 -2
- package/dist/social-providers/paybin.mjs.map +1 -0
- package/dist/social-providers/paypal.d.mts +2 -1
- package/dist/social-providers/paypal.mjs +2 -1
- package/dist/social-providers/paypal.mjs.map +1 -0
- package/dist/social-providers/polar.d.mts +2 -1
- package/dist/social-providers/polar.mjs +3 -2
- package/dist/social-providers/polar.mjs.map +1 -0
- package/dist/social-providers/railway.d.mts +68 -0
- package/dist/social-providers/railway.mjs +78 -0
- package/dist/social-providers/railway.mjs.map +1 -0
- package/dist/social-providers/reddit.d.mts +2 -1
- package/dist/social-providers/reddit.mjs +2 -1
- package/dist/social-providers/reddit.mjs.map +1 -0
- package/dist/social-providers/roblox.d.mts +2 -1
- package/dist/social-providers/roblox.mjs +2 -1
- package/dist/social-providers/roblox.mjs.map +1 -0
- package/dist/social-providers/salesforce.d.mts +2 -1
- package/dist/social-providers/salesforce.mjs +2 -1
- package/dist/social-providers/salesforce.mjs.map +1 -0
- package/dist/social-providers/slack.d.mts +2 -1
- package/dist/social-providers/slack.mjs +2 -1
- package/dist/social-providers/slack.mjs.map +1 -0
- package/dist/social-providers/spotify.d.mts +2 -1
- package/dist/social-providers/spotify.mjs +2 -1
- package/dist/social-providers/spotify.mjs.map +1 -0
- package/dist/social-providers/tiktok.d.mts +3 -3
- package/dist/social-providers/tiktok.mjs +3 -2
- package/dist/social-providers/tiktok.mjs.map +1 -0
- package/dist/social-providers/twitch.d.mts +2 -1
- package/dist/social-providers/twitch.mjs +2 -1
- package/dist/social-providers/twitch.mjs.map +1 -0
- package/dist/social-providers/twitter.d.mts +14 -25
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/twitter.mjs.map +1 -0
- package/dist/social-providers/vercel.d.mts +2 -1
- package/dist/social-providers/vercel.mjs +3 -2
- package/dist/social-providers/vercel.mjs.map +1 -0
- package/dist/social-providers/vk.d.mts +2 -1
- package/dist/social-providers/vk.mjs +2 -1
- package/dist/social-providers/vk.mjs.map +1 -0
- package/dist/social-providers/zoom.d.mts +3 -10
- package/dist/social-providers/zoom.mjs +2 -1
- package/dist/social-providers/zoom.mjs.map +1 -0
- package/dist/types/context.d.mts +54 -21
- package/dist/types/cookie.d.mts +2 -1
- package/dist/types/helper.d.mts +4 -1
- package/dist/types/index.d.mts +4 -3
- package/dist/types/init-options.d.mts +235 -144
- package/dist/types/plugin-client.d.mts +4 -1
- package/dist/types/plugin.d.mts +12 -11
- package/dist/types/secret.d.mts +12 -0
- package/dist/utils/db.d.mts +12 -0
- package/dist/utils/db.mjs +17 -0
- package/dist/utils/db.mjs.map +1 -0
- package/dist/utils/deprecate.d.mts +2 -2
- package/dist/utils/deprecate.mjs +2 -1
- package/dist/utils/deprecate.mjs.map +1 -0
- package/dist/utils/error-codes.d.mts +8 -6
- package/dist/utils/error-codes.mjs +3 -2
- package/dist/utils/error-codes.mjs.map +1 -0
- package/dist/utils/id.d.mts +2 -1
- package/dist/utils/id.mjs +2 -1
- package/dist/utils/id.mjs.map +1 -0
- package/dist/utils/ip.d.mts +55 -0
- package/dist/utils/ip.mjs +119 -0
- package/dist/utils/ip.mjs.map +1 -0
- package/dist/utils/json.d.mts +2 -1
- package/dist/utils/json.mjs +2 -1
- package/dist/utils/json.mjs.map +1 -0
- package/dist/utils/string.d.mts +2 -1
- package/dist/utils/string.mjs +2 -1
- package/dist/utils/string.mjs.map +1 -0
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +2 -1
- package/dist/utils/url.mjs.map +1 -0
- package/package.json +35 -13
- package/src/context/index.ts +1 -0
- package/src/context/transaction.ts +72 -9
- package/src/db/adapter/factory.ts +41 -73
- package/src/db/adapter/get-id-field.ts +1 -3
- package/src/db/adapter/index.ts +20 -15
- package/src/db/adapter/types.ts +2 -41
- package/src/db/get-tables.ts +48 -37
- package/src/db/index.ts +30 -5
- package/src/db/schema/account.ts +16 -3
- package/src/db/schema/rate-limit.ts +16 -1
- package/src/db/schema/session.ts +15 -3
- package/src/db/schema/user.ts +15 -3
- package/src/db/schema/verification.ts +16 -3
- package/src/db/test/get-tables.test.ts +33 -0
- package/src/db/type.ts +154 -1
- package/src/env/env-impl.ts +2 -2
- package/src/env/logger.ts +1 -1
- package/src/error/codes.ts +17 -0
- package/src/oauth2/client-credentials-token.ts +26 -2
- package/src/oauth2/create-authorization-url.ts +3 -1
- package/src/oauth2/index.ts +3 -0
- package/src/oauth2/oauth-provider.ts +1 -1
- package/src/oauth2/refresh-access-token.test.ts +90 -0
- package/src/oauth2/refresh-access-token.ts +37 -4
- package/src/oauth2/validate-authorization-code.ts +55 -29
- package/src/oauth2/validate-token.test.ts +229 -0
- package/src/social-providers/apple.ts +29 -29
- package/src/social-providers/cognito.ts +6 -5
- package/src/social-providers/dropbox.ts +1 -1
- package/src/social-providers/facebook.ts +3 -3
- package/src/social-providers/github.ts +26 -4
- package/src/social-providers/gitlab.ts +1 -1
- package/src/social-providers/google.ts +18 -14
- package/src/social-providers/huggingface.ts +1 -1
- package/src/social-providers/index.ts +9 -5
- package/src/social-providers/kakao.ts +1 -1
- package/src/social-providers/line.ts +1 -1
- package/src/social-providers/microsoft-entra-id.ts +84 -1
- package/src/social-providers/naver.ts +1 -1
- package/src/social-providers/notion.ts +1 -1
- package/src/social-providers/paybin.ts +1 -5
- package/src/social-providers/polar.ts +1 -1
- package/src/social-providers/railway.ts +100 -0
- package/src/social-providers/tiktok.ts +2 -1
- package/src/social-providers/vercel.ts +1 -1
- package/src/social-providers/zoom.ts +0 -8
- package/src/types/context.ts +79 -15
- package/src/types/helper.ts +9 -0
- package/src/types/index.ts +14 -2
- package/src/types/init-options.ts +298 -171
- package/src/types/plugin-client.ts +1 -0
- package/src/types/plugin.ts +11 -6
- package/src/types/secret.ts +8 -0
- package/src/utils/db.ts +20 -0
- package/src/utils/deprecate.test.ts +0 -1
- package/src/utils/error-codes.ts +12 -9
- package/src/utils/ip.test.ts +255 -0
- package/src/utils/ip.ts +211 -0
- package/.turbo/turbo-build.log +0 -180
- package/tsconfig.json +0 -7
- package/tsdown.config.ts +0 -32
- package/vitest.config.ts +0 -3
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { JWK } from "jose";
|
|
2
|
+
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { validateToken } from "./validate-authorization-code";
|
|
5
|
+
|
|
6
|
+
describe("validateToken", () => {
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
8
|
+
const mockedFetch = vi.fn() as unknown as typeof fetch &
|
|
9
|
+
ReturnType<typeof vi.fn>;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
globalThis.fetch = mockedFetch;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
globalThis.fetch = originalFetch;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function createTestJWKS(alg: string, crv?: string) {
|
|
20
|
+
const { publicKey, privateKey } = await generateKeyPair(alg, {
|
|
21
|
+
crv,
|
|
22
|
+
extractable: true,
|
|
23
|
+
});
|
|
24
|
+
const publicJWK = await exportJWK(publicKey);
|
|
25
|
+
const privateJWK = await exportJWK(privateKey);
|
|
26
|
+
const kid = `test-key-${Date.now()}`;
|
|
27
|
+
publicJWK.kid = kid;
|
|
28
|
+
publicJWK.alg = alg;
|
|
29
|
+
privateJWK.kid = kid;
|
|
30
|
+
privateJWK.alg = alg;
|
|
31
|
+
return { publicJWK, privateJWK, kid, publicKey, privateKey };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createSignedToken(
|
|
35
|
+
privateKey: CryptoKey,
|
|
36
|
+
alg: string,
|
|
37
|
+
kid: string,
|
|
38
|
+
payload: Record<string, unknown> = {},
|
|
39
|
+
) {
|
|
40
|
+
return await new SignJWT({
|
|
41
|
+
sub: "user-123",
|
|
42
|
+
email: "test@example.com",
|
|
43
|
+
iss: "https://example.com",
|
|
44
|
+
aud: "test-client",
|
|
45
|
+
...payload,
|
|
46
|
+
})
|
|
47
|
+
.setProtectedHeader({ alg, kid })
|
|
48
|
+
.setIssuedAt()
|
|
49
|
+
.setExpirationTime("1h")
|
|
50
|
+
.sign(privateKey);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mockJWKSResponse(...publicJWKs: JWK[]) {
|
|
54
|
+
mockedFetch.mockResolvedValueOnce(
|
|
55
|
+
new Response(JSON.stringify({ keys: publicJWKs }), {
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it("should verify RS256 signed token", async () => {
|
|
63
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
64
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
65
|
+
mockJWKSResponse(publicJWK);
|
|
66
|
+
|
|
67
|
+
const result = await validateToken(
|
|
68
|
+
token,
|
|
69
|
+
"https://example.com/.well-known/jwks",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result).toBeDefined();
|
|
73
|
+
expect(result.payload.sub).toBe("user-123");
|
|
74
|
+
expect(result.payload.email).toBe("test@example.com");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should verify ES256 signed token", async () => {
|
|
78
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("ES256");
|
|
79
|
+
const token = await createSignedToken(privateKey, "ES256", kid);
|
|
80
|
+
mockJWKSResponse(publicJWK);
|
|
81
|
+
|
|
82
|
+
const result = await validateToken(
|
|
83
|
+
token,
|
|
84
|
+
"https://example.com/.well-known/jwks",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result).toBeDefined();
|
|
88
|
+
expect(result.payload.sub).toBe("user-123");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should verify EdDSA (Ed25519) signed token", async () => {
|
|
92
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS(
|
|
93
|
+
"EdDSA",
|
|
94
|
+
"Ed25519",
|
|
95
|
+
);
|
|
96
|
+
const token = await createSignedToken(privateKey, "EdDSA", kid);
|
|
97
|
+
mockJWKSResponse(publicJWK);
|
|
98
|
+
|
|
99
|
+
const result = await validateToken(
|
|
100
|
+
token,
|
|
101
|
+
"https://example.com/.well-known/jwks",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result).toBeDefined();
|
|
105
|
+
expect(result.payload.sub).toBe("user-123");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should throw when kid doesn't match any key", async () => {
|
|
109
|
+
const { publicJWK, privateKey } = await createTestJWKS("RS256");
|
|
110
|
+
publicJWK.kid = "different-kid";
|
|
111
|
+
const token = await createSignedToken(privateKey, "RS256", "original-kid");
|
|
112
|
+
mockJWKSResponse(publicJWK);
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
116
|
+
).rejects.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should find correct key when multiple keys exist", async () => {
|
|
120
|
+
const key1 = await createTestJWKS("RS256");
|
|
121
|
+
const key2 = await createTestJWKS("RS256");
|
|
122
|
+
const key3 = await createTestJWKS("ES256");
|
|
123
|
+
const token = await createSignedToken(key2.privateKey, "RS256", key2.kid);
|
|
124
|
+
mockJWKSResponse(key1.publicJWK, key2.publicJWK, key3.publicJWK);
|
|
125
|
+
|
|
126
|
+
const result = await validateToken(
|
|
127
|
+
token,
|
|
128
|
+
"https://example.com/.well-known/jwks",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result).toBeDefined();
|
|
132
|
+
expect(result.payload.sub).toBe("user-123");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should throw when JWKS returns empty keys array", async () => {
|
|
136
|
+
const { privateKey, kid } = await createTestJWKS("RS256");
|
|
137
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
138
|
+
mockJWKSResponse();
|
|
139
|
+
|
|
140
|
+
await expect(
|
|
141
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
142
|
+
).rejects.toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should throw when JWKS fetch fails", async () => {
|
|
146
|
+
const { privateKey, kid } = await createTestJWKS("RS256");
|
|
147
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
148
|
+
mockedFetch.mockResolvedValueOnce(
|
|
149
|
+
new Response("Internal Server Error", { status: 500 }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
154
|
+
).rejects.toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should verify token with matching audience", async () => {
|
|
158
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
159
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
160
|
+
mockJWKSResponse(publicJWK);
|
|
161
|
+
|
|
162
|
+
const result = await validateToken(
|
|
163
|
+
token,
|
|
164
|
+
"https://example.com/.well-known/jwks",
|
|
165
|
+
{ audience: "test-client" },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(result).toBeDefined();
|
|
169
|
+
expect(result.payload.aud).toBe("test-client");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should reject token with mismatched audience", async () => {
|
|
173
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
174
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
175
|
+
mockJWKSResponse(publicJWK);
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
validateToken(token, "https://example.com/.well-known/jwks", {
|
|
179
|
+
audience: "wrong-client",
|
|
180
|
+
}),
|
|
181
|
+
).rejects.toThrow();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should verify token with matching issuer", async () => {
|
|
185
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
186
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
187
|
+
mockJWKSResponse(publicJWK);
|
|
188
|
+
|
|
189
|
+
const result = await validateToken(
|
|
190
|
+
token,
|
|
191
|
+
"https://example.com/.well-known/jwks",
|
|
192
|
+
{ issuer: "https://example.com" },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result).toBeDefined();
|
|
196
|
+
expect(result.payload.iss).toBe("https://example.com");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should reject token with mismatched issuer", async () => {
|
|
200
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
201
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
202
|
+
mockJWKSResponse(publicJWK);
|
|
203
|
+
|
|
204
|
+
await expect(
|
|
205
|
+
validateToken(token, "https://example.com/.well-known/jwks", {
|
|
206
|
+
issuer: "https://wrong-issuer.com",
|
|
207
|
+
}),
|
|
208
|
+
).rejects.toThrow();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should verify token with both audience and issuer", async () => {
|
|
212
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
213
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
214
|
+
mockJWKSResponse(publicJWK);
|
|
215
|
+
|
|
216
|
+
const result = await validateToken(
|
|
217
|
+
token,
|
|
218
|
+
"https://example.com/.well-known/jwks",
|
|
219
|
+
{
|
|
220
|
+
audience: "test-client",
|
|
221
|
+
issuer: "https://example.com",
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(result).toBeDefined();
|
|
226
|
+
expect(result.payload.aud).toBe("test-client");
|
|
227
|
+
expect(result.payload.iss).toBe("https://example.com");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -112,41 +112,41 @@ export const apple = (options: AppleOptions) => {
|
|
|
112
112
|
if (options.verifyIdToken) {
|
|
113
113
|
return options.verifyIdToken(token, nonce);
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
jwtClaims[field]
|
|
115
|
+
try {
|
|
116
|
+
const decodedHeader = decodeProtectedHeader(token);
|
|
117
|
+
const { kid, alg: jwtAlg } = decodedHeader;
|
|
118
|
+
if (!kid || !jwtAlg) return false;
|
|
119
|
+
const publicKey = await getApplePublicKey(kid);
|
|
120
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
121
|
+
algorithms: [jwtAlg],
|
|
122
|
+
issuer: "https://appleid.apple.com",
|
|
123
|
+
audience:
|
|
124
|
+
options.audience && options.audience.length
|
|
125
|
+
? options.audience
|
|
126
|
+
: options.appBundleIdentifier
|
|
127
|
+
? options.appBundleIdentifier
|
|
128
|
+
: options.clientId,
|
|
129
|
+
maxTokenAge: "1h",
|
|
130
|
+
});
|
|
131
|
+
["email_verified", "is_private_email"].forEach((field) => {
|
|
132
|
+
if (jwtClaims[field] !== undefined) {
|
|
133
|
+
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
137
|
+
return false;
|
|
133
138
|
}
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
return !!jwtClaims;
|
|
140
|
+
} catch {
|
|
136
141
|
return false;
|
|
137
142
|
}
|
|
138
|
-
return !!jwtClaims;
|
|
139
143
|
},
|
|
140
144
|
refreshAccessToken: options.refreshAccessToken
|
|
141
145
|
? options.refreshAccessToken
|
|
142
146
|
: async (refreshToken) => {
|
|
143
147
|
return refreshAccessToken({
|
|
144
148
|
refreshToken,
|
|
145
|
-
options
|
|
146
|
-
clientId: options.clientId,
|
|
147
|
-
clientKey: options.clientKey,
|
|
148
|
-
clientSecret: options.clientSecret,
|
|
149
|
-
},
|
|
149
|
+
options,
|
|
150
150
|
tokenEndpoint: "https://appleid.apple.com/auth/token",
|
|
151
151
|
});
|
|
152
152
|
},
|
|
@@ -162,15 +162,15 @@ export const apple = (options: AppleOptions) => {
|
|
|
162
162
|
return null;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// TODO: "
|
|
165
|
+
// TODO: "" masking will be removed when the name field is made optional
|
|
166
166
|
let name: string;
|
|
167
167
|
if (token.user?.name) {
|
|
168
168
|
const firstName = token.user.name.firstName || "";
|
|
169
169
|
const lastName = token.user.name.lastName || "";
|
|
170
170
|
const fullName = `${firstName} ${lastName}`.trim();
|
|
171
|
-
name = fullName
|
|
171
|
+
name = fullName;
|
|
172
172
|
} else {
|
|
173
|
-
name = profile.name || "
|
|
173
|
+
name = profile.name || "";
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const emailVerified =
|
|
@@ -178,10 +178,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
178
178
|
return null;
|
|
179
179
|
}
|
|
180
180
|
const name =
|
|
181
|
-
profile.name ||
|
|
182
|
-
profile.given_name ||
|
|
183
|
-
profile.username ||
|
|
184
|
-
profile.email;
|
|
181
|
+
profile.name || profile.given_name || profile.username || "";
|
|
185
182
|
const enrichedProfile = {
|
|
186
183
|
...profile,
|
|
187
184
|
name,
|
|
@@ -220,7 +217,11 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
220
217
|
return {
|
|
221
218
|
user: {
|
|
222
219
|
id: userInfo.sub,
|
|
223
|
-
name:
|
|
220
|
+
name:
|
|
221
|
+
userInfo.name ||
|
|
222
|
+
userInfo.given_name ||
|
|
223
|
+
userInfo.username ||
|
|
224
|
+
"",
|
|
224
225
|
email: userInfo.email,
|
|
225
226
|
image: userInfo.picture,
|
|
226
227
|
emailVerified: userInfo.email_verified,
|
|
@@ -49,7 +49,7 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
49
49
|
return await createAuthorizationURL({
|
|
50
50
|
id: "facebook",
|
|
51
51
|
options,
|
|
52
|
-
authorizationEndpoint: "https://www.facebook.com/
|
|
52
|
+
authorizationEndpoint: "https://www.facebook.com/v24.0/dialog/oauth",
|
|
53
53
|
scopes: _scopes,
|
|
54
54
|
state,
|
|
55
55
|
redirectURI,
|
|
@@ -66,7 +66,7 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
66
66
|
code,
|
|
67
67
|
redirectURI,
|
|
68
68
|
options,
|
|
69
|
-
tokenEndpoint: "https://graph.facebook.com/oauth/access_token",
|
|
69
|
+
tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
|
|
70
70
|
});
|
|
71
71
|
},
|
|
72
72
|
async verifyIdToken(token, nonce) {
|
|
@@ -121,7 +121,7 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
121
121
|
clientSecret: options.clientSecret,
|
|
122
122
|
},
|
|
123
123
|
tokenEndpoint:
|
|
124
|
-
"https://graph.facebook.com/
|
|
124
|
+
"https://graph.facebook.com/v24.0/oauth/access_token",
|
|
125
125
|
});
|
|
126
126
|
},
|
|
127
127
|
async getUserInfo(token) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
+
import { logger } from "../env";
|
|
2
3
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
4
|
import {
|
|
4
5
|
createAuthorizationURL,
|
|
6
|
+
getOAuth2Tokens,
|
|
5
7
|
refreshAccessToken,
|
|
6
|
-
validateAuthorizationCode,
|
|
7
8
|
} from "../oauth2";
|
|
9
|
+
import { createAuthorizationCodeRequest } from "../oauth2/validate-authorization-code";
|
|
8
10
|
|
|
9
11
|
export interface GithubProfile {
|
|
10
12
|
login: string;
|
|
@@ -86,13 +88,33 @@ export const github = (options: GithubOptions) => {
|
|
|
86
88
|
});
|
|
87
89
|
},
|
|
88
90
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
89
|
-
|
|
91
|
+
const { body, headers: requestHeaders } = createAuthorizationCodeRequest({
|
|
90
92
|
code,
|
|
91
93
|
codeVerifier,
|
|
92
94
|
redirectURI,
|
|
93
95
|
options,
|
|
94
|
-
tokenEndpoint,
|
|
95
96
|
});
|
|
97
|
+
|
|
98
|
+
const { data, error } = await betterFetch<
|
|
99
|
+
| { access_token: string; token_type: string; scope: string }
|
|
100
|
+
| { error: string; error_description?: string; error_uri?: string }
|
|
101
|
+
>(tokenEndpoint, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: body,
|
|
104
|
+
headers: requestHeaders,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (error) {
|
|
108
|
+
logger.error("GitHub OAuth token exchange failed:", error);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if ("error" in data) {
|
|
113
|
+
logger.error("GitHub OAuth token exchange failed:", data);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return getOAuth2Tokens(data);
|
|
96
118
|
},
|
|
97
119
|
refreshAccessToken: options.refreshAccessToken
|
|
98
120
|
? options.refreshAccessToken
|
|
@@ -148,7 +170,7 @@ export const github = (options: GithubOptions) => {
|
|
|
148
170
|
return {
|
|
149
171
|
user: {
|
|
150
172
|
id: profile.id,
|
|
151
|
-
name: profile.name || profile.login,
|
|
173
|
+
name: profile.name || profile.login || "",
|
|
152
174
|
email: profile.email,
|
|
153
175
|
image: profile.avatar_url,
|
|
154
176
|
emailVerified,
|
|
@@ -141,7 +141,7 @@ export const gitlab = (options: GitlabOptions) => {
|
|
|
141
141
|
return {
|
|
142
142
|
user: {
|
|
143
143
|
id: profile.id,
|
|
144
|
-
name: profile.name ?? profile.username,
|
|
144
|
+
name: profile.name ?? profile.username ?? "",
|
|
145
145
|
email: profile.email,
|
|
146
146
|
image: profile.avatar_url,
|
|
147
147
|
emailVerified: profile.email_verified ?? false,
|
|
@@ -81,7 +81,7 @@ export const google = (options: GoogleOptions) => {
|
|
|
81
81
|
const url = await createAuthorizationURL({
|
|
82
82
|
id: "google",
|
|
83
83
|
options,
|
|
84
|
-
authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
|
|
84
|
+
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
85
85
|
scopes: _scopes,
|
|
86
86
|
state,
|
|
87
87
|
codeVerifier,
|
|
@@ -116,7 +116,7 @@ export const google = (options: GoogleOptions) => {
|
|
|
116
116
|
clientKey: options.clientKey,
|
|
117
117
|
clientSecret: options.clientSecret,
|
|
118
118
|
},
|
|
119
|
-
tokenEndpoint: "https://
|
|
119
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
120
120
|
});
|
|
121
121
|
},
|
|
122
122
|
async verifyIdToken(token, nonce) {
|
|
@@ -130,22 +130,26 @@ export const google = (options: GoogleOptions) => {
|
|
|
130
130
|
// Verify JWT integrity
|
|
131
131
|
// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
try {
|
|
134
|
+
const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
|
|
135
|
+
if (!kid || !jwtAlg) return false;
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
const publicKey = await getGooglePublicKey(kid);
|
|
138
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
139
|
+
algorithms: [jwtAlg],
|
|
140
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
141
|
+
audience: options.clientId,
|
|
142
|
+
maxTokenAge: "1h",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
145
151
|
return false;
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
return true;
|
|
149
153
|
},
|
|
150
154
|
async getUserInfo(token) {
|
|
151
155
|
if (options.getUserInfo) {
|
|
@@ -104,7 +104,7 @@ export const huggingface = (options: HuggingFaceOptions) => {
|
|
|
104
104
|
return {
|
|
105
105
|
user: {
|
|
106
106
|
id: profile.sub,
|
|
107
|
-
name: profile.name || profile.preferred_username,
|
|
107
|
+
name: profile.name || profile.preferred_username || "",
|
|
108
108
|
email: profile.email,
|
|
109
109
|
image: profile.picture,
|
|
110
110
|
emailVerified: profile.email_verified ?? false,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import type { AwaitableFunction } from "../types";
|
|
2
3
|
import { apple } from "./apple";
|
|
3
4
|
import { atlassian } from "./atlassian";
|
|
4
5
|
import { cognito } from "./cognito";
|
|
@@ -21,6 +22,7 @@ import { notion } from "./notion";
|
|
|
21
22
|
import { paybin } from "./paybin";
|
|
22
23
|
import { paypal } from "./paypal";
|
|
23
24
|
import { polar } from "./polar";
|
|
25
|
+
import { railway } from "./railway";
|
|
24
26
|
import { reddit } from "./reddit";
|
|
25
27
|
import { roblox } from "./roblox";
|
|
26
28
|
import { salesforce } from "./salesforce";
|
|
@@ -66,6 +68,7 @@ export const socialProviders = {
|
|
|
66
68
|
paybin,
|
|
67
69
|
paypal,
|
|
68
70
|
polar,
|
|
71
|
+
railway,
|
|
69
72
|
vercel,
|
|
70
73
|
};
|
|
71
74
|
|
|
@@ -81,11 +84,11 @@ export const SocialProviderListEnum = z
|
|
|
81
84
|
export type SocialProvider = z.infer<typeof SocialProviderListEnum>;
|
|
82
85
|
|
|
83
86
|
export type SocialProviders = {
|
|
84
|
-
[K in SocialProviderList[number]]?:
|
|
85
|
-
(typeof socialProviders)[K]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
[K in SocialProviderList[number]]?: AwaitableFunction<
|
|
88
|
+
Parameters<(typeof socialProviders)[K]>[0] & {
|
|
89
|
+
enabled?: boolean | undefined;
|
|
90
|
+
}
|
|
91
|
+
>;
|
|
89
92
|
};
|
|
90
93
|
|
|
91
94
|
export * from "./apple";
|
|
@@ -112,6 +115,7 @@ export * from "./notion";
|
|
|
112
115
|
export * from "./paybin";
|
|
113
116
|
export * from "./paypal";
|
|
114
117
|
export * from "./polar";
|
|
118
|
+
export * from "./railway";
|
|
115
119
|
export * from "./reddit";
|
|
116
120
|
export * from "./roblox";
|
|
117
121
|
export * from "./salesforce";
|
|
@@ -161,7 +161,7 @@ export const kakao = (options: KakaoOptions) => {
|
|
|
161
161
|
const kakaoProfile = account.profile || {};
|
|
162
162
|
const user = {
|
|
163
163
|
id: String(profile.id),
|
|
164
|
-
name: kakaoProfile.nickname || account.name ||
|
|
164
|
+
name: kakaoProfile.nickname || account.name || "",
|
|
165
165
|
email: account.email,
|
|
166
166
|
image:
|
|
167
167
|
kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url,
|
|
@@ -147,7 +147,7 @@ export const line = (options: LineOptions) => {
|
|
|
147
147
|
const userMap = await options.mapProfileToUser?.(profile as any);
|
|
148
148
|
// ID preference order
|
|
149
149
|
const id = (profile as any).sub || (profile as any).userId;
|
|
150
|
-
const name = (profile as any).name || (profile as any).displayName;
|
|
150
|
+
const name = (profile as any).name || (profile as any).displayName || "";
|
|
151
151
|
const image =
|
|
152
152
|
(profile as any).picture || (profile as any).pictureUrl || undefined;
|
|
153
153
|
const email = (profile as any).email;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { base64 } from "@better-auth/utils/base64";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import { decodeJwt } from "jose";
|
|
3
|
+
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
4
4
|
import { logger } from "../env";
|
|
5
|
+
import { APIError } from "../error";
|
|
5
6
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
7
|
import {
|
|
7
8
|
createAuthorizationURL,
|
|
@@ -174,6 +175,56 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
174
175
|
tokenEndpoint,
|
|
175
176
|
});
|
|
176
177
|
},
|
|
178
|
+
async verifyIdToken(token, nonce) {
|
|
179
|
+
if (options.disableIdTokenSignIn) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (options.verifyIdToken) {
|
|
183
|
+
return options.verifyIdToken(token, nonce);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
|
|
188
|
+
if (!kid || !jwtAlg) return false;
|
|
189
|
+
|
|
190
|
+
const publicKey = await getMicrosoftPublicKey(kid, tenant, authority);
|
|
191
|
+
const verifyOptions: {
|
|
192
|
+
algorithms: [string];
|
|
193
|
+
audience: string;
|
|
194
|
+
maxTokenAge: string;
|
|
195
|
+
issuer?: string;
|
|
196
|
+
} = {
|
|
197
|
+
algorithms: [jwtAlg],
|
|
198
|
+
audience: options.clientId,
|
|
199
|
+
maxTokenAge: "1h",
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Issuer varies per user's tenant for multi-tenant endpoints, so only validate for specific tenants.
|
|
203
|
+
* @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
|
|
204
|
+
*/
|
|
205
|
+
if (
|
|
206
|
+
tenant !== "common" &&
|
|
207
|
+
tenant !== "organizations" &&
|
|
208
|
+
tenant !== "consumers"
|
|
209
|
+
) {
|
|
210
|
+
verifyOptions.issuer = `${authority}/${tenant}/v2.0`;
|
|
211
|
+
}
|
|
212
|
+
const { payload: jwtClaims } = await jwtVerify(
|
|
213
|
+
token,
|
|
214
|
+
publicKey,
|
|
215
|
+
verifyOptions,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error("Failed to verify ID token:", error);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
177
228
|
async getUserInfo(token) {
|
|
178
229
|
if (options.getUserInfo) {
|
|
179
230
|
return options.getUserInfo(token);
|
|
@@ -257,3 +308,35 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
257
308
|
options,
|
|
258
309
|
} satisfies OAuthProvider;
|
|
259
310
|
};
|
|
311
|
+
|
|
312
|
+
export const getMicrosoftPublicKey = async (
|
|
313
|
+
kid: string,
|
|
314
|
+
tenant: string,
|
|
315
|
+
authority: string,
|
|
316
|
+
) => {
|
|
317
|
+
const { data } = await betterFetch<{
|
|
318
|
+
keys: Array<{
|
|
319
|
+
kid: string;
|
|
320
|
+
alg: string;
|
|
321
|
+
kty: string;
|
|
322
|
+
use: string;
|
|
323
|
+
n: string;
|
|
324
|
+
e: string;
|
|
325
|
+
x5c?: string[];
|
|
326
|
+
x5t?: string;
|
|
327
|
+
}>;
|
|
328
|
+
}>(`${authority}/${tenant}/discovery/v2.0/keys`);
|
|
329
|
+
|
|
330
|
+
if (!data?.keys) {
|
|
331
|
+
throw new APIError("BAD_REQUEST", {
|
|
332
|
+
message: "Keys not found",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const jwk = data.keys.find((key) => key.kid === kid);
|
|
337
|
+
if (!jwk) {
|
|
338
|
+
throw new Error(`JWK with kid ${kid} not found`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return await importJWK(jwk, jwk.alg);
|
|
342
|
+
};
|