@better-auth/core 1.7.0-beta.1 → 1.7.0-beta.10
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/dist/api/index.d.mts +44 -1
- package/dist/api/index.mjs +69 -4
- package/dist/context/global.mjs +1 -1
- package/dist/context/transaction.d.mts +7 -4
- package/dist/context/transaction.mjs +6 -3
- package/dist/db/adapter/factory.mjs +91 -3
- package/dist/db/adapter/get-id-field.mjs +1 -1
- package/dist/db/adapter/index.d.mts +87 -3
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +2 -1
- package/dist/db/type.d.mts +17 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +1 -0
- package/dist/error/codes.mjs +1 -0
- package/dist/error/index.d.mts +7 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/api.mjs +12 -0
- package/dist/instrumentation/noop.mjs +42 -0
- package/dist/instrumentation/pure.index.d.mts +7 -0
- package/dist/instrumentation/pure.index.mjs +7 -0
- package/dist/instrumentation/tracer.mjs +6 -3
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +20 -31
- package/dist/oauth2/create-authorization-url.d.mts +11 -1
- package/dist/oauth2/create-authorization-url.mjs +27 -7
- package/dist/oauth2/dpop.d.mts +142 -0
- package/dist/oauth2/dpop.mjs +246 -0
- package/dist/oauth2/index.d.mts +14 -9
- package/dist/oauth2/index.mjs +12 -8
- package/dist/oauth2/oauth-provider.d.mts +144 -10
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +38 -36
- package/dist/oauth2/reject-redirects.mjs +65 -0
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +23 -1
- package/dist/oauth2/utils.mjs +48 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +28 -34
- package/dist/oauth2/verify-id-token.d.mts +27 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +88 -15
- package/dist/oauth2/verify.mjs +203 -25
- package/dist/social-providers/apple.d.mts +16 -4
- package/dist/social-providers/apple.mjs +17 -24
- package/dist/social-providers/atlassian.d.mts +5 -1
- package/dist/social-providers/atlassian.mjs +6 -3
- package/dist/social-providers/cognito.d.mts +27 -3
- package/dist/social-providers/cognito.mjs +15 -25
- package/dist/social-providers/discord.d.mts +7 -3
- package/dist/social-providers/discord.mjs +16 -3
- package/dist/social-providers/dropbox.d.mts +5 -1
- package/dist/social-providers/dropbox.mjs +5 -4
- package/dist/social-providers/facebook.d.mts +24 -5
- package/dist/social-providers/facebook.mjs +54 -20
- package/dist/social-providers/figma.d.mts +5 -1
- package/dist/social-providers/figma.mjs +4 -3
- package/dist/social-providers/github.d.mts +6 -2
- package/dist/social-providers/github.mjs +5 -4
- package/dist/social-providers/gitlab.d.mts +5 -1
- package/dist/social-providers/gitlab.mjs +3 -2
- package/dist/social-providers/google.d.mts +53 -5
- package/dist/social-providers/google.mjs +69 -25
- package/dist/social-providers/huggingface.d.mts +5 -1
- package/dist/social-providers/huggingface.mjs +3 -2
- package/dist/social-providers/index.d.mts +221 -45
- package/dist/social-providers/index.mjs +2 -2
- package/dist/social-providers/kakao.d.mts +5 -1
- package/dist/social-providers/kakao.mjs +3 -2
- package/dist/social-providers/kick.d.mts +5 -1
- package/dist/social-providers/kick.mjs +3 -2
- package/dist/social-providers/line.d.mts +8 -2
- package/dist/social-providers/line.mjs +5 -6
- package/dist/social-providers/linear.d.mts +5 -1
- package/dist/social-providers/linear.mjs +3 -2
- package/dist/social-providers/linkedin.d.mts +7 -3
- package/dist/social-providers/linkedin.mjs +4 -3
- package/dist/social-providers/microsoft-entra-id.d.mts +41 -7
- package/dist/social-providers/microsoft-entra-id.mjs +37 -28
- package/dist/social-providers/naver.d.mts +5 -1
- package/dist/social-providers/naver.mjs +3 -2
- package/dist/social-providers/notion.d.mts +5 -1
- package/dist/social-providers/notion.mjs +5 -2
- package/dist/social-providers/paybin.d.mts +5 -1
- package/dist/social-providers/paybin.mjs +4 -3
- package/dist/social-providers/paypal.d.mts +6 -2
- package/dist/social-providers/paypal.mjs +19 -14
- package/dist/social-providers/polar.d.mts +5 -1
- package/dist/social-providers/polar.mjs +3 -2
- package/dist/social-providers/railway.d.mts +5 -1
- package/dist/social-providers/railway.mjs +3 -2
- package/dist/social-providers/reddit.d.mts +5 -1
- package/dist/social-providers/reddit.mjs +7 -5
- package/dist/social-providers/roblox.d.mts +6 -2
- package/dist/social-providers/roblox.mjs +12 -2
- package/dist/social-providers/salesforce.d.mts +5 -1
- package/dist/social-providers/salesforce.mjs +4 -3
- package/dist/social-providers/slack.d.mts +6 -2
- package/dist/social-providers/slack.mjs +11 -8
- package/dist/social-providers/spotify.d.mts +5 -1
- package/dist/social-providers/spotify.mjs +3 -2
- package/dist/social-providers/tiktok.d.mts +5 -1
- package/dist/social-providers/tiktok.mjs +14 -2
- package/dist/social-providers/twitch.d.mts +5 -1
- package/dist/social-providers/twitch.mjs +3 -2
- package/dist/social-providers/twitter.d.mts +4 -1
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/vercel.d.mts +5 -1
- package/dist/social-providers/vercel.mjs +3 -2
- package/dist/social-providers/vk.d.mts +5 -1
- package/dist/social-providers/vk.mjs +3 -2
- package/dist/social-providers/wechat.d.mts +5 -1
- package/dist/social-providers/wechat.mjs +8 -2
- package/dist/social-providers/zoom.d.mts +6 -2
- package/dist/social-providers/zoom.mjs +10 -17
- package/dist/types/context.d.mts +55 -6
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +182 -12
- package/dist/types/plugin-client.d.mts +12 -2
- package/dist/utils/async.d.mts +22 -0
- package/dist/utils/async.mjs +32 -0
- package/dist/utils/host.d.mts +147 -0
- package/dist/utils/host.mjs +298 -0
- package/dist/utils/ip.d.mts +28 -5
- package/dist/utils/ip.mjs +118 -4
- package/dist/utils/is-api-error.d.mts +6 -0
- package/dist/utils/is-api-error.mjs +8 -0
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +34 -4
- package/package.json +26 -16
- package/src/api/index.ts +121 -5
- package/src/context/transaction.ts +45 -12
- package/src/db/adapter/factory.ts +193 -7
- package/src/db/adapter/get-id-field.ts +2 -2
- package/src/db/adapter/index.ts +85 -2
- package/src/db/adapter/types.ts +2 -0
- package/src/db/get-tables.ts +9 -1
- package/src/db/schema/account.ts +4 -1
- package/src/db/schema/user.ts +3 -0
- package/src/db/type.ts +17 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +1 -0
- package/src/error/index.ts +9 -0
- package/src/instrumentation/api.ts +17 -0
- package/src/instrumentation/noop.ts +74 -0
- package/src/instrumentation/pure.index.ts +31 -0
- package/src/instrumentation/tracer.ts +8 -3
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +50 -74
- package/src/oauth2/create-authorization-url.ts +33 -7
- package/src/oauth2/dpop.ts +568 -0
- package/src/oauth2/index.ts +81 -11
- package/src/oauth2/oauth-provider.ts +153 -11
- package/src/oauth2/refresh-access-token.ts +82 -83
- package/src/oauth2/reject-redirects.ts +75 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +72 -5
- package/src/oauth2/validate-authorization-code.ts +69 -89
- package/src/oauth2/verify-id-token.ts +115 -0
- package/src/oauth2/verify.ts +409 -78
- package/src/social-providers/apple.ts +30 -40
- package/src/social-providers/atlassian.ts +8 -1
- package/src/social-providers/cognito.ts +36 -39
- package/src/social-providers/discord.ts +22 -18
- package/src/social-providers/dropbox.ts +7 -5
- package/src/social-providers/facebook.ts +108 -52
- package/src/social-providers/figma.ts +8 -1
- package/src/social-providers/github.ts +5 -3
- package/src/social-providers/gitlab.ts +2 -0
- package/src/social-providers/google.ts +141 -39
- package/src/social-providers/huggingface.ts +8 -1
- package/src/social-providers/kakao.ts +2 -1
- package/src/social-providers/kick.ts +8 -1
- package/src/social-providers/line.ts +25 -27
- package/src/social-providers/linear.ts +8 -1
- package/src/social-providers/linkedin.ts +5 -3
- package/src/social-providers/microsoft-entra-id.ts +100 -55
- package/src/social-providers/naver.ts +2 -1
- package/src/social-providers/notion.ts +8 -1
- package/src/social-providers/paybin.ts +2 -0
- package/src/social-providers/paypal.ts +28 -17
- package/src/social-providers/polar.ts +8 -1
- package/src/social-providers/railway.ts +8 -1
- package/src/social-providers/reddit.ts +9 -4
- package/src/social-providers/roblox.ts +16 -11
- package/src/social-providers/salesforce.ts +8 -1
- package/src/social-providers/slack.ts +15 -9
- package/src/social-providers/spotify.ts +8 -1
- package/src/social-providers/tiktok.ts +22 -9
- package/src/social-providers/twitch.ts +2 -1
- package/src/social-providers/twitter.ts +1 -0
- package/src/social-providers/vercel.ts +8 -1
- package/src/social-providers/vk.ts +8 -1
- package/src/social-providers/wechat.ts +17 -2
- package/src/social-providers/zoom.ts +15 -19
- package/src/types/context.ts +57 -5
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +204 -14
- package/src/types/plugin-client.ts +16 -2
- package/src/utils/async.ts +53 -0
- package/src/utils/host.ts +425 -0
- package/src/utils/ip.ts +197 -13
- package/src/utils/is-api-error.ts +10 -0
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +38 -4
|
@@ -35,7 +35,13 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
35
35
|
id: "atlassian",
|
|
36
36
|
name: "Atlassian",
|
|
37
37
|
|
|
38
|
-
async createAuthorizationURL({
|
|
38
|
+
async createAuthorizationURL({
|
|
39
|
+
state,
|
|
40
|
+
scopes,
|
|
41
|
+
codeVerifier,
|
|
42
|
+
redirectURI,
|
|
43
|
+
additionalParams,
|
|
44
|
+
}) {
|
|
39
45
|
if (!options.clientId || !options.clientSecret) {
|
|
40
46
|
logger.error("Client Id and Secret are required for Atlassian");
|
|
41
47
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
@@ -59,6 +65,7 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
59
65
|
codeVerifier,
|
|
60
66
|
redirectURI,
|
|
61
67
|
additionalParams: {
|
|
68
|
+
...(additionalParams ?? {}),
|
|
62
69
|
audience: "api.atlassian.com",
|
|
63
70
|
},
|
|
64
71
|
prompt: options.prompt,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import { decodeJwt,
|
|
2
|
+
import { decodeJwt, importJWK } from "jose";
|
|
3
3
|
import { logger } from "../env";
|
|
4
4
|
import { APIError, BetterAuthError } from "../error";
|
|
5
5
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
|
+
getPrimaryClientId,
|
|
8
9
|
refreshAccessToken,
|
|
9
10
|
validateAuthorizationCode,
|
|
10
11
|
} from "../oauth2";
|
|
@@ -30,7 +31,7 @@ export interface CognitoProfile {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
33
|
-
clientId: string;
|
|
34
|
+
clientId: string | string[];
|
|
34
35
|
/**
|
|
35
36
|
* The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com")
|
|
36
37
|
*/
|
|
@@ -41,6 +42,19 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
|
41
42
|
region: string;
|
|
42
43
|
userPoolId: string;
|
|
43
44
|
requireClientSecret?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Skip the Cognito hosted-UI identity-provider picker by preselecting an
|
|
47
|
+
* IdP (maps to the `identity_provider` query parameter on the authorize
|
|
48
|
+
* request). Accepts `"COGNITO"`, a SAML/OIDC provider name configured on
|
|
49
|
+
* the User Pool, or one of the social providers (`"Google"`, `"Facebook"`,
|
|
50
|
+
* `"LoginWithAmazon"`, `"SignInWithApple"`).
|
|
51
|
+
*
|
|
52
|
+
* Per-request overrides via `signIn.social({ additionalParams: { identity_provider } })`
|
|
53
|
+
* take precedence over this value.
|
|
54
|
+
*
|
|
55
|
+
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
|
|
56
|
+
*/
|
|
57
|
+
identityProvider?: string | undefined;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export const cognito = (options: CognitoOptions) => {
|
|
@@ -59,8 +73,14 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
59
73
|
return {
|
|
60
74
|
id: "cognito",
|
|
61
75
|
name: "Cognito",
|
|
62
|
-
async createAuthorizationURL({
|
|
63
|
-
|
|
76
|
+
async createAuthorizationURL({
|
|
77
|
+
state,
|
|
78
|
+
scopes,
|
|
79
|
+
codeVerifier,
|
|
80
|
+
redirectURI,
|
|
81
|
+
additionalParams,
|
|
82
|
+
}) {
|
|
83
|
+
if (!getPrimaryClientId(options.clientId)) {
|
|
64
84
|
logger.error(
|
|
65
85
|
"ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
|
|
66
86
|
);
|
|
@@ -90,6 +110,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
90
110
|
codeVerifier,
|
|
91
111
|
redirectURI,
|
|
92
112
|
prompt: options.prompt,
|
|
113
|
+
additionalParams: {
|
|
114
|
+
...(options.identityProvider
|
|
115
|
+
? { identity_provider: options.identityProvider }
|
|
116
|
+
: {}),
|
|
117
|
+
...(additionalParams ?? {}),
|
|
118
|
+
},
|
|
93
119
|
});
|
|
94
120
|
// AWS Cognito requires scopes to be encoded with %20 instead of +
|
|
95
121
|
// URLSearchParams encodes spaces as + by default, so we need to fix this
|
|
@@ -129,41 +155,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
129
155
|
});
|
|
130
156
|
},
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const decodedHeader = decodeProtectedHeader(token);
|
|
142
|
-
const { kid, alg: jwtAlg } = decodedHeader;
|
|
143
|
-
if (!kid || !jwtAlg) return false;
|
|
144
|
-
|
|
145
|
-
const publicKey = await getCognitoPublicKey(
|
|
146
|
-
kid,
|
|
147
|
-
options.region,
|
|
148
|
-
options.userPoolId,
|
|
149
|
-
);
|
|
150
|
-
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
|
|
151
|
-
|
|
152
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
153
|
-
algorithms: [jwtAlg],
|
|
154
|
-
issuer: expectedIssuer,
|
|
155
|
-
audience: options.clientId,
|
|
156
|
-
maxTokenAge: "1h",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
logger.error("Failed to verify ID token:", error);
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
158
|
+
idToken: {
|
|
159
|
+
jwks: (header) =>
|
|
160
|
+
getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
|
|
161
|
+
issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
|
|
162
|
+
audience: options.clientId,
|
|
163
|
+
maxTokenAge: "1h",
|
|
167
164
|
},
|
|
168
165
|
|
|
169
166
|
async getUserInfo(token) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createAuthorizationURL,
|
|
5
|
+
refreshAccessToken,
|
|
6
|
+
validateAuthorizationCode,
|
|
7
|
+
} from "../oauth2";
|
|
4
8
|
export interface DiscordProfile extends Record<string, any> {
|
|
5
9
|
/** the user's id (i.e. the numerical snowflake) */
|
|
6
10
|
id: string;
|
|
@@ -41,7 +45,7 @@ export interface DiscordProfile extends Record<string, any> {
|
|
|
41
45
|
/** whether the email on this account has been verified */
|
|
42
46
|
verified: boolean;
|
|
43
47
|
/** the user's email */
|
|
44
|
-
email
|
|
48
|
+
email?: string | null;
|
|
45
49
|
/**
|
|
46
50
|
* the flags on a user's account:
|
|
47
51
|
* https://discord.com/developers/docs/resources/user#user-object-user-flags
|
|
@@ -84,26 +88,26 @@ export const discord = (options: DiscordOptions) => {
|
|
|
84
88
|
return {
|
|
85
89
|
id: "discord",
|
|
86
90
|
name: "Discord",
|
|
87
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
91
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
88
92
|
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
|
|
89
93
|
if (scopes) _scopes.push(...scopes);
|
|
90
94
|
if (options.scope) _scopes.push(...options.scope);
|
|
91
95
|
const hasBotScope = _scopes.includes("bot");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
);
|
|
96
|
+
return createAuthorizationURL({
|
|
97
|
+
id: "discord",
|
|
98
|
+
options,
|
|
99
|
+
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
|
|
100
|
+
scopes: _scopes,
|
|
101
|
+
state,
|
|
102
|
+
redirectURI,
|
|
103
|
+
prompt: options.prompt || "none",
|
|
104
|
+
additionalParams: {
|
|
105
|
+
...(hasBotScope && options.permissions !== undefined
|
|
106
|
+
? { permissions: String(options.permissions) }
|
|
107
|
+
: {}),
|
|
108
|
+
...(additionalParams ?? {}),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
107
111
|
},
|
|
108
112
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
109
113
|
return validateAuthorizationCode({
|
|
@@ -36,14 +36,11 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
36
36
|
scopes,
|
|
37
37
|
codeVerifier,
|
|
38
38
|
redirectURI,
|
|
39
|
+
additionalParams,
|
|
39
40
|
}) => {
|
|
40
41
|
const _scopes = options.disableDefaultScope ? [] : ["account_info.read"];
|
|
41
42
|
if (options.scope) _scopes.push(...options.scope);
|
|
42
43
|
if (scopes) _scopes.push(...scopes);
|
|
43
|
-
const additionalParams: Record<string, string> = {};
|
|
44
|
-
if (options.accessType) {
|
|
45
|
-
additionalParams.token_access_type = options.accessType;
|
|
46
|
-
}
|
|
47
44
|
return await createAuthorizationURL({
|
|
48
45
|
id: "dropbox",
|
|
49
46
|
options,
|
|
@@ -52,7 +49,12 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
52
49
|
state,
|
|
53
50
|
redirectURI,
|
|
54
51
|
codeVerifier,
|
|
55
|
-
additionalParams
|
|
52
|
+
additionalParams: {
|
|
53
|
+
...(options.accessType
|
|
54
|
+
? { token_access_type: options.accessType }
|
|
55
|
+
: {}),
|
|
56
|
+
...(additionalParams ?? {}),
|
|
57
|
+
},
|
|
56
58
|
});
|
|
57
59
|
},
|
|
58
60
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import { createRemoteJWKSet, decodeJwt
|
|
2
|
+
import { createRemoteJWKSet, decodeJwt } from "jose";
|
|
3
|
+
import { logger } from "../env";
|
|
4
|
+
import { BetterAuthError } from "../error";
|
|
3
5
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
4
6
|
import {
|
|
5
7
|
createAuthorizationURL,
|
|
8
|
+
getPrimaryClientId,
|
|
6
9
|
refreshAccessToken,
|
|
7
10
|
validateAuthorizationCode,
|
|
8
11
|
} from "../oauth2";
|
|
9
12
|
export interface FacebookProfile {
|
|
10
13
|
id: string;
|
|
11
14
|
name: string;
|
|
12
|
-
email
|
|
13
|
-
email_verified
|
|
15
|
+
email?: string;
|
|
16
|
+
email_verified?: boolean;
|
|
14
17
|
picture: {
|
|
15
18
|
data: {
|
|
16
19
|
height: number;
|
|
@@ -21,8 +24,60 @@ export interface FacebookProfile {
|
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
interface FacebookDebugTokenData {
|
|
28
|
+
app_id?: string;
|
|
29
|
+
is_valid?: boolean;
|
|
30
|
+
user_id?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate an opaque Facebook access token against the configured app.
|
|
35
|
+
*
|
|
36
|
+
* Facebook access tokens are not audience-bound at the Graph `/me` endpoint: a
|
|
37
|
+
* token minted for any Facebook app returns that app's profile. Without this
|
|
38
|
+
* check, a token issued to an unrelated app could be presented to this
|
|
39
|
+
* app's direct sign-in path and accepted as proof of identity. We call the
|
|
40
|
+
* `debug_token` endpoint and require the token to be valid, bound to one of the
|
|
41
|
+
* configured client ids, and tied to a user.
|
|
42
|
+
*
|
|
43
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/access-tokens/debugging
|
|
44
|
+
*
|
|
45
|
+
* @returns the inspected token's `user_id` when the token is valid and bound to
|
|
46
|
+
* the configured app, otherwise `null`.
|
|
47
|
+
*/
|
|
48
|
+
async function verifyFacebookAccessToken(
|
|
49
|
+
accessToken: string,
|
|
50
|
+
options: FacebookOptions,
|
|
51
|
+
): Promise<string | null> {
|
|
52
|
+
const primaryClientId = getPrimaryClientId(options.clientId);
|
|
53
|
+
if (!primaryClientId || !options.clientSecret) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const clientIds = Array.isArray(options.clientId)
|
|
57
|
+
? options.clientId
|
|
58
|
+
: [options.clientId];
|
|
59
|
+
const appAccessToken = `${primaryClientId}|${options.clientSecret}`;
|
|
60
|
+
const { data, error } = await betterFetch<{ data?: FacebookDebugTokenData }>(
|
|
61
|
+
"https://graph.facebook.com/debug_token",
|
|
62
|
+
{
|
|
63
|
+
query: {
|
|
64
|
+
input_token: accessToken,
|
|
65
|
+
access_token: appAccessToken,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
if (error || !data?.data) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const { is_valid, app_id, user_id } = data.data;
|
|
73
|
+
if (is_valid !== true || !app_id || !clientIds.includes(app_id) || !user_id) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return user_id;
|
|
77
|
+
}
|
|
78
|
+
|
|
24
79
|
export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
25
|
-
clientId: string;
|
|
80
|
+
clientId: string | string[];
|
|
26
81
|
/**
|
|
27
82
|
* Extend list of fields to retrieve from the Facebook user profile.
|
|
28
83
|
*
|
|
@@ -40,7 +95,19 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
40
95
|
return {
|
|
41
96
|
id: "facebook",
|
|
42
97
|
name: "Facebook",
|
|
43
|
-
async createAuthorizationURL({
|
|
98
|
+
async createAuthorizationURL({
|
|
99
|
+
state,
|
|
100
|
+
scopes,
|
|
101
|
+
redirectURI,
|
|
102
|
+
loginHint,
|
|
103
|
+
additionalParams,
|
|
104
|
+
}) {
|
|
105
|
+
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
106
|
+
logger.error(
|
|
107
|
+
"Client ID and client secret are required for Facebook. Make sure to provide them in the options.",
|
|
108
|
+
);
|
|
109
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
110
|
+
}
|
|
44
111
|
const _scopes = options.disableDefaultScope
|
|
45
112
|
? []
|
|
46
113
|
: ["email", "public_profile"];
|
|
@@ -54,11 +121,10 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
54
121
|
state,
|
|
55
122
|
redirectURI,
|
|
56
123
|
loginHint,
|
|
57
|
-
additionalParams:
|
|
58
|
-
? {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: {},
|
|
124
|
+
additionalParams: {
|
|
125
|
+
...(options.configId ? { config_id: options.configId } : {}),
|
|
126
|
+
...(additionalParams ?? {}),
|
|
127
|
+
},
|
|
62
128
|
});
|
|
63
129
|
},
|
|
64
130
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -69,46 +135,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
69
135
|
tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
|
|
70
136
|
});
|
|
71
137
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (token.split(".").length === 3) {
|
|
84
|
-
try {
|
|
85
|
-
const { payload: jwtClaims } = await jwtVerify(
|
|
86
|
-
token,
|
|
87
|
-
createRemoteJWKSet(
|
|
88
|
-
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
89
|
-
new URL(
|
|
90
|
-
"https://limited.facebook.com/.well-known/oauth/openid/jwks/",
|
|
91
|
-
),
|
|
92
|
-
),
|
|
93
|
-
{
|
|
94
|
-
algorithms: ["RS256"],
|
|
95
|
-
audience: options.clientId,
|
|
96
|
-
issuer: "https://www.facebook.com",
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return !!jwtClaims;
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* access_token */
|
|
111
|
-
return true;
|
|
138
|
+
idToken: {
|
|
139
|
+
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
140
|
+
jwks: createRemoteJWKSet(
|
|
141
|
+
new URL("https://limited.facebook.com/.well-known/oauth/openid/jwks/"),
|
|
142
|
+
),
|
|
143
|
+
issuer: "https://www.facebook.com",
|
|
144
|
+
audience: options.clientId,
|
|
145
|
+
algorithms: ["RS256"],
|
|
146
|
+
// Facebook also accepts an opaque Graph access token on the client sign-in path;
|
|
147
|
+
// identity is then resolved by getUserInfo via the Graph API, which validates it.
|
|
148
|
+
allowOpaqueToken: true,
|
|
112
149
|
},
|
|
113
150
|
refreshAccessToken: options.refreshAccessToken
|
|
114
151
|
? options.refreshAccessToken
|
|
@@ -169,6 +206,21 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
169
206
|
};
|
|
170
207
|
}
|
|
171
208
|
|
|
209
|
+
// The profile is fetched with `accessToken`, which is the credential
|
|
210
|
+
// that actually proves identity here. It is a separate request field
|
|
211
|
+
// from the `idToken` checked by the shared id_token verifier via the
|
|
212
|
+
// declarative `idToken` config. Since an opaque token is not app-bound
|
|
213
|
+
// at `/me`, validate this exact token against the configured app
|
|
214
|
+
// before trusting the profile it returns.
|
|
215
|
+
const accessToken = token.accessToken;
|
|
216
|
+
if (!accessToken) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const tokenUserId = await verifyFacebookAccessToken(accessToken, options);
|
|
220
|
+
if (!tokenUserId) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
172
224
|
const fields = [
|
|
173
225
|
"id",
|
|
174
226
|
"name",
|
|
@@ -181,13 +233,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
181
233
|
{
|
|
182
234
|
auth: {
|
|
183
235
|
type: "Bearer",
|
|
184
|
-
token:
|
|
236
|
+
token: accessToken,
|
|
185
237
|
},
|
|
186
238
|
},
|
|
187
239
|
);
|
|
188
240
|
if (error) {
|
|
189
241
|
return null;
|
|
190
242
|
}
|
|
243
|
+
// Bind the validated token to the profile it returned.
|
|
244
|
+
if (profile.id !== tokenUserId) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
191
247
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
192
248
|
return {
|
|
193
249
|
user: {
|
|
@@ -195,7 +251,7 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
195
251
|
name: profile.name,
|
|
196
252
|
email: profile.email,
|
|
197
253
|
image: profile.picture.data.url,
|
|
198
|
-
emailVerified: profile.email_verified,
|
|
254
|
+
emailVerified: profile.email_verified ?? false,
|
|
199
255
|
...userMap,
|
|
200
256
|
},
|
|
201
257
|
data: profile,
|
|
@@ -24,7 +24,13 @@ export const figma = (options: FigmaOptions) => {
|
|
|
24
24
|
return {
|
|
25
25
|
id: "figma",
|
|
26
26
|
name: "Figma",
|
|
27
|
-
async createAuthorizationURL({
|
|
27
|
+
async createAuthorizationURL({
|
|
28
|
+
state,
|
|
29
|
+
scopes,
|
|
30
|
+
codeVerifier,
|
|
31
|
+
redirectURI,
|
|
32
|
+
additionalParams,
|
|
33
|
+
}) {
|
|
28
34
|
if (!options.clientId || !options.clientSecret) {
|
|
29
35
|
logger.error(
|
|
30
36
|
"Client Id and Client Secret are required for Figma. Make sure to provide them in the options.",
|
|
@@ -47,6 +53,7 @@ export const figma = (options: FigmaOptions) => {
|
|
|
47
53
|
state,
|
|
48
54
|
codeVerifier,
|
|
49
55
|
redirectURI,
|
|
56
|
+
additionalParams,
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
return url;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getOAuth2Tokens,
|
|
7
7
|
refreshAccessToken,
|
|
8
8
|
} from "../oauth2";
|
|
9
|
-
import {
|
|
9
|
+
import { authorizationCodeRequest } from "../oauth2/validate-authorization-code";
|
|
10
10
|
|
|
11
11
|
export interface GithubProfile {
|
|
12
12
|
login: string;
|
|
@@ -31,7 +31,7 @@ export interface GithubProfile {
|
|
|
31
31
|
company: string;
|
|
32
32
|
blog: string;
|
|
33
33
|
location: string;
|
|
34
|
-
email: string;
|
|
34
|
+
email: string | null;
|
|
35
35
|
hireable: boolean;
|
|
36
36
|
bio: string;
|
|
37
37
|
twitter_username: string;
|
|
@@ -69,6 +69,7 @@ export const github = (options: GithubOptions) => {
|
|
|
69
69
|
loginHint,
|
|
70
70
|
codeVerifier,
|
|
71
71
|
redirectURI,
|
|
72
|
+
additionalParams,
|
|
72
73
|
}) {
|
|
73
74
|
const _scopes = options.disableDefaultScope
|
|
74
75
|
? []
|
|
@@ -85,10 +86,11 @@ export const github = (options: GithubOptions) => {
|
|
|
85
86
|
redirectURI,
|
|
86
87
|
loginHint,
|
|
87
88
|
prompt: options.prompt,
|
|
89
|
+
additionalParams,
|
|
88
90
|
});
|
|
89
91
|
},
|
|
90
92
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
91
|
-
const { body, headers: requestHeaders } =
|
|
93
|
+
const { body, headers: requestHeaders } = await authorizationCodeRequest({
|
|
92
94
|
code,
|
|
93
95
|
codeVerifier,
|
|
94
96
|
redirectURI,
|
|
@@ -87,6 +87,7 @@ export const gitlab = (options: GitlabOptions) => {
|
|
|
87
87
|
codeVerifier,
|
|
88
88
|
loginHint,
|
|
89
89
|
redirectURI,
|
|
90
|
+
additionalParams,
|
|
90
91
|
}) => {
|
|
91
92
|
const _scopes = options.disableDefaultScope ? [] : ["read_user"];
|
|
92
93
|
if (options.scope) _scopes.push(...options.scope);
|
|
@@ -100,6 +101,7 @@ export const gitlab = (options: GitlabOptions) => {
|
|
|
100
101
|
redirectURI,
|
|
101
102
|
codeVerifier,
|
|
102
103
|
loginHint,
|
|
104
|
+
additionalParams,
|
|
103
105
|
});
|
|
104
106
|
},
|
|
105
107
|
validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
|