@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5
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 +3 -3
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +62 -0
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +1 -1
- package/dist/db/schema/account.mjs +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +6 -0
- package/dist/error/codes.mjs +6 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- 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 +18 -29
- package/dist/oauth2/create-authorization-url.d.mts +13 -2
- package/dist/oauth2/create-authorization-url.mjs +28 -7
- package/dist/oauth2/index.d.mts +13 -8
- package/dist/oauth2/index.mjs +11 -7
- package/dist/oauth2/oauth-provider.d.mts +149 -11
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +20 -33
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -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 +9 -1
- package/dist/oauth2/utils.mjs +14 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify-id-token.d.mts +26 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +14 -0
- package/dist/oauth2/verify.mjs +38 -12
- package/dist/social-providers/apple.d.mts +18 -20
- package/dist/social-providers/apple.mjs +15 -28
- package/dist/social-providers/atlassian.d.mts +8 -2
- package/dist/social-providers/atlassian.mjs +9 -6
- package/dist/social-providers/cognito.d.mts +29 -3
- package/dist/social-providers/cognito.mjs +30 -34
- package/dist/social-providers/discord.d.mts +8 -2
- package/dist/social-providers/discord.mjs +20 -6
- package/dist/social-providers/dropbox.d.mts +8 -2
- package/dist/social-providers/dropbox.mjs +10 -9
- package/dist/social-providers/facebook.d.mts +24 -3
- package/dist/social-providers/facebook.mjs +51 -24
- package/dist/social-providers/figma.d.mts +8 -2
- package/dist/social-providers/figma.mjs +8 -7
- package/dist/social-providers/github.d.mts +8 -2
- package/dist/social-providers/github.mjs +9 -8
- package/dist/social-providers/gitlab.d.mts +8 -2
- package/dist/social-providers/gitlab.mjs +8 -7
- package/dist/social-providers/google.d.mts +32 -4
- package/dist/social-providers/google.mjs +26 -29
- package/dist/social-providers/huggingface.d.mts +8 -2
- package/dist/social-providers/huggingface.mjs +11 -10
- package/dist/social-providers/index.d.mts +322 -75
- package/dist/social-providers/kakao.d.mts +8 -2
- package/dist/social-providers/kakao.mjs +11 -10
- package/dist/social-providers/kick.d.mts +8 -2
- package/dist/social-providers/kick.mjs +7 -6
- package/dist/social-providers/line.d.mts +11 -3
- package/dist/social-providers/line.mjs +14 -15
- package/dist/social-providers/linear.d.mts +8 -2
- package/dist/social-providers/linear.mjs +7 -6
- package/dist/social-providers/linkedin.d.mts +8 -2
- package/dist/social-providers/linkedin.mjs +12 -11
- package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
- package/dist/social-providers/microsoft-entra-id.mjs +28 -38
- package/dist/social-providers/naver.d.mts +8 -2
- package/dist/social-providers/naver.mjs +7 -6
- package/dist/social-providers/notion.d.mts +8 -2
- package/dist/social-providers/notion.mjs +9 -6
- package/dist/social-providers/paybin.d.mts +8 -2
- package/dist/social-providers/paybin.mjs +12 -11
- package/dist/social-providers/paypal.d.mts +8 -3
- package/dist/social-providers/paypal.mjs +10 -14
- package/dist/social-providers/polar.d.mts +8 -2
- package/dist/social-providers/polar.mjs +11 -10
- package/dist/social-providers/railway.d.mts +8 -2
- package/dist/social-providers/railway.mjs +11 -10
- package/dist/social-providers/reddit.d.mts +8 -2
- package/dist/social-providers/reddit.mjs +11 -9
- package/dist/social-providers/roblox.d.mts +8 -2
- package/dist/social-providers/roblox.mjs +15 -5
- package/dist/social-providers/salesforce.d.mts +8 -2
- package/dist/social-providers/salesforce.mjs +11 -10
- package/dist/social-providers/slack.d.mts +8 -2
- package/dist/social-providers/slack.mjs +18 -15
- package/dist/social-providers/spotify.d.mts +8 -2
- package/dist/social-providers/spotify.mjs +7 -6
- package/dist/social-providers/tiktok.d.mts +8 -2
- package/dist/social-providers/tiktok.mjs +21 -5
- package/dist/social-providers/twitch.d.mts +8 -2
- package/dist/social-providers/twitch.mjs +7 -6
- package/dist/social-providers/twitter.d.mts +7 -2
- package/dist/social-providers/twitter.mjs +11 -10
- package/dist/social-providers/vercel.d.mts +8 -2
- package/dist/social-providers/vercel.mjs +7 -9
- package/dist/social-providers/vk.d.mts +8 -2
- package/dist/social-providers/vk.mjs +7 -6
- package/dist/social-providers/wechat.d.mts +8 -2
- package/dist/social-providers/wechat.mjs +16 -6
- package/dist/social-providers/zoom.d.mts +10 -3
- package/dist/social-providers/zoom.mjs +14 -15
- package/dist/types/context.d.mts +33 -11
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +121 -6
- package/dist/utils/ip.d.mts +5 -4
- package/dist/utils/ip.mjs +3 -3
- 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 +30 -1
- package/package.json +13 -12
- package/src/db/adapter/factory.ts +126 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/db/type.ts +12 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +6 -0
- 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 +48 -72
- package/src/oauth2/create-authorization-url.ts +30 -8
- package/src/oauth2/index.ts +42 -10
- package/src/oauth2/oauth-provider.ts +161 -12
- package/src/oauth2/refresh-access-token.ts +52 -78
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +21 -5
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +82 -15
- package/src/social-providers/apple.ts +32 -45
- package/src/social-providers/atlassian.ts +20 -9
- package/src/social-providers/cognito.ts +51 -48
- package/src/social-providers/discord.ts +37 -22
- package/src/social-providers/dropbox.ts +20 -12
- package/src/social-providers/facebook.ts +108 -57
- package/src/social-providers/figma.ts +21 -10
- package/src/social-providers/github.ts +16 -10
- package/src/social-providers/gitlab.ts +16 -8
- package/src/social-providers/google.ts +67 -46
- package/src/social-providers/huggingface.ts +20 -9
- package/src/social-providers/kakao.ts +18 -9
- package/src/social-providers/kick.ts +20 -8
- package/src/social-providers/line.ts +39 -37
- package/src/social-providers/linear.ts +20 -7
- package/src/social-providers/linkedin.ts +16 -10
- package/src/social-providers/microsoft-entra-id.ts +66 -64
- package/src/social-providers/naver.ts +14 -7
- package/src/social-providers/notion.ts +20 -7
- package/src/social-providers/paybin.ts +16 -11
- package/src/social-providers/paypal.ts +12 -25
- package/src/social-providers/polar.ts +20 -9
- package/src/social-providers/railway.ts +20 -9
- package/src/social-providers/reddit.ts +22 -10
- package/src/social-providers/roblox.ts +31 -15
- package/src/social-providers/salesforce.ts +21 -10
- package/src/social-providers/slack.ts +31 -16
- package/src/social-providers/spotify.ts +20 -7
- package/src/social-providers/tiktok.ts +32 -13
- package/src/social-providers/twitch.ts +14 -9
- package/src/social-providers/twitter.ts +18 -8
- package/src/social-providers/vercel.ts +24 -11
- package/src/social-providers/vk.ts +20 -7
- package/src/social-providers/wechat.ts +28 -8
- package/src/social-providers/zoom.ts +28 -19
- package/src/types/context.ts +33 -12
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +148 -5
- package/src/utils/ip.ts +12 -13
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +28 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import { createRemoteJWKSet, decodeJwt
|
|
2
|
+
import { createRemoteJWKSet, decodeJwt } from "jose";
|
|
3
3
|
import { logger } from "../env";
|
|
4
4
|
import { BetterAuthError } from "../error";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
8
|
getPrimaryClientId,
|
|
9
9
|
refreshAccessToken,
|
|
10
|
+
resolveRequestedScopes,
|
|
10
11
|
validateAuthorizationCode,
|
|
11
12
|
} from "../oauth2";
|
|
12
13
|
export interface FacebookProfile {
|
|
@@ -24,6 +25,58 @@ export interface FacebookProfile {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
interface FacebookDebugTokenData {
|
|
29
|
+
app_id?: string;
|
|
30
|
+
is_valid?: boolean;
|
|
31
|
+
user_id?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate an opaque Facebook access token against the configured app.
|
|
36
|
+
*
|
|
37
|
+
* Facebook access tokens are not audience-bound at the Graph `/me` endpoint: a
|
|
38
|
+
* token minted for any Facebook app returns that app's profile. Without this
|
|
39
|
+
* check, a token issued to an unrelated app could be presented to this
|
|
40
|
+
* app's direct sign-in path and accepted as proof of identity. We call the
|
|
41
|
+
* `debug_token` endpoint and require the token to be valid, bound to one of the
|
|
42
|
+
* configured client ids, and tied to a user.
|
|
43
|
+
*
|
|
44
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/access-tokens/debugging
|
|
45
|
+
*
|
|
46
|
+
* @returns the inspected token's `user_id` when the token is valid and bound to
|
|
47
|
+
* the configured app, otherwise `null`.
|
|
48
|
+
*/
|
|
49
|
+
async function verifyFacebookAccessToken(
|
|
50
|
+
accessToken: string,
|
|
51
|
+
options: FacebookOptions,
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
const primaryClientId = getPrimaryClientId(options.clientId);
|
|
54
|
+
if (!primaryClientId || !options.clientSecret) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const clientIds = Array.isArray(options.clientId)
|
|
58
|
+
? options.clientId
|
|
59
|
+
: [options.clientId];
|
|
60
|
+
const appAccessToken = `${primaryClientId}|${options.clientSecret}`;
|
|
61
|
+
const { data, error } = await betterFetch<{ data?: FacebookDebugTokenData }>(
|
|
62
|
+
"https://graph.facebook.com/debug_token",
|
|
63
|
+
{
|
|
64
|
+
query: {
|
|
65
|
+
input_token: accessToken,
|
|
66
|
+
access_token: appAccessToken,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
if (error || !data?.data) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const { is_valid, app_id, user_id } = data.data;
|
|
74
|
+
if (is_valid !== true || !app_id || !clientIds.includes(app_id) || !user_id) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return user_id;
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
28
81
|
clientId: string | string[];
|
|
29
82
|
/**
|
|
@@ -39,35 +92,43 @@ export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
|
39
92
|
configId?: string | undefined;
|
|
40
93
|
}
|
|
41
94
|
|
|
95
|
+
const FACEBOOK_DEFAULT_SCOPES = ["email", "public_profile"];
|
|
96
|
+
|
|
42
97
|
export const facebook = (options: FacebookOptions) => {
|
|
43
98
|
return {
|
|
44
99
|
id: "facebook",
|
|
45
100
|
name: "Facebook",
|
|
46
|
-
|
|
101
|
+
callbackPath: "/callback/facebook",
|
|
102
|
+
async createAuthorizationURL({
|
|
103
|
+
state,
|
|
104
|
+
scopes,
|
|
105
|
+
redirectURI,
|
|
106
|
+
loginHint,
|
|
107
|
+
additionalParams,
|
|
108
|
+
}) {
|
|
47
109
|
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
48
110
|
logger.error(
|
|
49
111
|
"Client ID and client secret are required for Facebook. Make sure to provide them in the options.",
|
|
50
112
|
);
|
|
51
113
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
52
114
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
115
|
+
const requestedScopes = resolveRequestedScopes(
|
|
116
|
+
options,
|
|
117
|
+
FACEBOOK_DEFAULT_SCOPES,
|
|
118
|
+
scopes,
|
|
119
|
+
);
|
|
120
|
+
return createAuthorizationURL({
|
|
59
121
|
id: "facebook",
|
|
60
122
|
options,
|
|
61
123
|
authorizationEndpoint: "https://www.facebook.com/v24.0/dialog/oauth",
|
|
62
|
-
scopes:
|
|
124
|
+
scopes: requestedScopes,
|
|
63
125
|
state,
|
|
64
126
|
redirectURI,
|
|
65
127
|
loginHint,
|
|
66
|
-
additionalParams:
|
|
67
|
-
? {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: {},
|
|
128
|
+
additionalParams: {
|
|
129
|
+
...(options.configId ? { config_id: options.configId } : {}),
|
|
130
|
+
...(additionalParams ?? {}),
|
|
131
|
+
},
|
|
71
132
|
});
|
|
72
133
|
},
|
|
73
134
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -78,46 +139,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
78
139
|
tokenEndpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
|
|
79
140
|
});
|
|
80
141
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (token.split(".").length === 3) {
|
|
93
|
-
try {
|
|
94
|
-
const { payload: jwtClaims } = await jwtVerify(
|
|
95
|
-
token,
|
|
96
|
-
createRemoteJWKSet(
|
|
97
|
-
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
98
|
-
new URL(
|
|
99
|
-
"https://limited.facebook.com/.well-known/oauth/openid/jwks/",
|
|
100
|
-
),
|
|
101
|
-
),
|
|
102
|
-
{
|
|
103
|
-
algorithms: ["RS256"],
|
|
104
|
-
audience: options.clientId,
|
|
105
|
-
issuer: "https://www.facebook.com",
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return !!jwtClaims;
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* access_token */
|
|
120
|
-
return true;
|
|
142
|
+
idToken: {
|
|
143
|
+
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
|
|
144
|
+
jwks: createRemoteJWKSet(
|
|
145
|
+
new URL("https://limited.facebook.com/.well-known/oauth/openid/jwks/"),
|
|
146
|
+
),
|
|
147
|
+
issuer: "https://www.facebook.com",
|
|
148
|
+
audience: options.clientId,
|
|
149
|
+
algorithms: ["RS256"],
|
|
150
|
+
// Facebook also accepts an opaque Graph access token on the client sign-in path;
|
|
151
|
+
// identity is then resolved by getUserInfo via the Graph API, which validates it.
|
|
152
|
+
allowOpaqueToken: true,
|
|
121
153
|
},
|
|
122
154
|
refreshAccessToken: options.refreshAccessToken
|
|
123
155
|
? options.refreshAccessToken
|
|
@@ -178,6 +210,21 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
178
210
|
};
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
// The profile is fetched with `accessToken`, which is the credential
|
|
214
|
+
// that actually proves identity here. It is a separate request field
|
|
215
|
+
// from the `idToken` checked by the shared id_token verifier via the
|
|
216
|
+
// declarative `idToken` config. Since an opaque token is not app-bound
|
|
217
|
+
// at `/me`, validate this exact token against the configured app
|
|
218
|
+
// before trusting the profile it returns.
|
|
219
|
+
const accessToken = token.accessToken;
|
|
220
|
+
if (!accessToken) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const tokenUserId = await verifyFacebookAccessToken(accessToken, options);
|
|
224
|
+
if (!tokenUserId) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
181
228
|
const fields = [
|
|
182
229
|
"id",
|
|
183
230
|
"name",
|
|
@@ -190,13 +237,17 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
190
237
|
{
|
|
191
238
|
auth: {
|
|
192
239
|
type: "Bearer",
|
|
193
|
-
token:
|
|
240
|
+
token: accessToken,
|
|
194
241
|
},
|
|
195
242
|
},
|
|
196
243
|
);
|
|
197
244
|
if (error) {
|
|
198
245
|
return null;
|
|
199
246
|
}
|
|
247
|
+
// Bind the validated token to the profile it returned.
|
|
248
|
+
if (profile.id !== tokenUserId) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
200
251
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
201
252
|
return {
|
|
202
253
|
user: {
|
|
@@ -211,5 +262,5 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
211
262
|
};
|
|
212
263
|
},
|
|
213
264
|
options,
|
|
214
|
-
} satisfies
|
|
265
|
+
} satisfies UpstreamProvider<FacebookProfile>;
|
|
215
266
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import { logger } from "../env";
|
|
3
3
|
import { BetterAuthError } from "../error";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
5
5
|
import {
|
|
6
6
|
createAuthorizationURL,
|
|
7
7
|
refreshAccessToken,
|
|
8
|
+
resolveRequestedScopes,
|
|
8
9
|
validateAuthorizationCode,
|
|
9
10
|
} from "../oauth2";
|
|
10
11
|
|
|
@@ -19,12 +20,21 @@ export interface FigmaOptions extends ProviderOptions<FigmaProfile> {
|
|
|
19
20
|
clientId: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
const FIGMA_DEFAULT_SCOPES = ["current_user:read"];
|
|
24
|
+
|
|
22
25
|
export const figma = (options: FigmaOptions) => {
|
|
23
26
|
const tokenEndpoint = "https://api.figma.com/v1/oauth/token";
|
|
24
27
|
return {
|
|
25
28
|
id: "figma",
|
|
26
29
|
name: "Figma",
|
|
27
|
-
|
|
30
|
+
callbackPath: "/callback/figma",
|
|
31
|
+
async createAuthorizationURL({
|
|
32
|
+
state,
|
|
33
|
+
scopes,
|
|
34
|
+
codeVerifier,
|
|
35
|
+
redirectURI,
|
|
36
|
+
additionalParams,
|
|
37
|
+
}) {
|
|
28
38
|
if (!options.clientId || !options.clientSecret) {
|
|
29
39
|
logger.error(
|
|
30
40
|
"Client Id and Client Secret are required for Figma. Make sure to provide them in the options.",
|
|
@@ -35,21 +45,22 @@ export const figma = (options: FigmaOptions) => {
|
|
|
35
45
|
throw new BetterAuthError("codeVerifier is required for Figma");
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
const requestedScopes = resolveRequestedScopes(
|
|
49
|
+
options,
|
|
50
|
+
FIGMA_DEFAULT_SCOPES,
|
|
51
|
+
scopes,
|
|
52
|
+
);
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
return createAuthorizationURL({
|
|
43
55
|
id: "figma",
|
|
44
56
|
options,
|
|
45
57
|
authorizationEndpoint: "https://www.figma.com/oauth",
|
|
46
|
-
scopes:
|
|
58
|
+
scopes: requestedScopes,
|
|
47
59
|
state,
|
|
48
60
|
codeVerifier,
|
|
49
61
|
redirectURI,
|
|
62
|
+
additionalParams,
|
|
50
63
|
});
|
|
51
|
-
|
|
52
|
-
return url;
|
|
53
64
|
},
|
|
54
65
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
55
66
|
return validateAuthorizationCode({
|
|
@@ -114,5 +125,5 @@ export const figma = (options: FigmaOptions) => {
|
|
|
114
125
|
}
|
|
115
126
|
},
|
|
116
127
|
options,
|
|
117
|
-
} satisfies
|
|
128
|
+
} satisfies UpstreamProvider<FigmaProfile>;
|
|
118
129
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import { logger } from "../env";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
4
4
|
import {
|
|
5
5
|
createAuthorizationURL,
|
|
6
6
|
getOAuth2Tokens,
|
|
7
7
|
refreshAccessToken,
|
|
8
|
+
resolveRequestedScopes,
|
|
8
9
|
} from "../oauth2";
|
|
9
|
-
import {
|
|
10
|
+
import { authorizationCodeRequest } from "../oauth2/validate-authorization-code";
|
|
10
11
|
|
|
11
12
|
export interface GithubProfile {
|
|
12
13
|
login: string;
|
|
@@ -58,37 +59,42 @@ export interface GithubProfile {
|
|
|
58
59
|
export interface GithubOptions extends ProviderOptions<GithubProfile> {
|
|
59
60
|
clientId: string;
|
|
60
61
|
}
|
|
62
|
+
const GITHUB_DEFAULT_SCOPES = ["read:user", "user:email"];
|
|
63
|
+
|
|
61
64
|
export const github = (options: GithubOptions) => {
|
|
62
65
|
const tokenEndpoint = "https://github.com/login/oauth/access_token";
|
|
63
66
|
return {
|
|
64
67
|
id: "github",
|
|
65
68
|
name: "GitHub",
|
|
69
|
+
callbackPath: "/callback/github",
|
|
66
70
|
createAuthorizationURL({
|
|
67
71
|
state,
|
|
68
72
|
scopes,
|
|
69
73
|
loginHint,
|
|
70
74
|
codeVerifier,
|
|
71
75
|
redirectURI,
|
|
76
|
+
additionalParams,
|
|
72
77
|
}) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const requestedScopes = resolveRequestedScopes(
|
|
79
|
+
options,
|
|
80
|
+
GITHUB_DEFAULT_SCOPES,
|
|
81
|
+
scopes,
|
|
82
|
+
);
|
|
78
83
|
return createAuthorizationURL({
|
|
79
84
|
id: "github",
|
|
80
85
|
options,
|
|
81
86
|
authorizationEndpoint: "https://github.com/login/oauth/authorize",
|
|
82
|
-
scopes:
|
|
87
|
+
scopes: requestedScopes,
|
|
83
88
|
state,
|
|
84
89
|
codeVerifier,
|
|
85
90
|
redirectURI,
|
|
86
91
|
loginHint,
|
|
87
92
|
prompt: options.prompt,
|
|
93
|
+
additionalParams,
|
|
88
94
|
});
|
|
89
95
|
},
|
|
90
96
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
91
|
-
const { body, headers: requestHeaders } =
|
|
97
|
+
const { body, headers: requestHeaders } = await authorizationCodeRequest({
|
|
92
98
|
code,
|
|
93
99
|
codeVerifier,
|
|
94
100
|
redirectURI,
|
|
@@ -180,5 +186,5 @@ export const github = (options: GithubOptions) => {
|
|
|
180
186
|
};
|
|
181
187
|
},
|
|
182
188
|
options,
|
|
183
|
-
} satisfies
|
|
189
|
+
} satisfies UpstreamProvider<GithubProfile>;
|
|
184
190
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
3
3
|
import {
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
+
resolveRequestedScopes,
|
|
6
7
|
validateAuthorizationCode,
|
|
7
8
|
} from "../oauth2";
|
|
8
9
|
|
|
@@ -73,6 +74,8 @@ const issuerToEndpoints = (issuer?: string | undefined) => {
|
|
|
73
74
|
};
|
|
74
75
|
};
|
|
75
76
|
|
|
77
|
+
const GITLAB_DEFAULT_SCOPES = ["read_user"];
|
|
78
|
+
|
|
76
79
|
export const gitlab = (options: GitlabOptions) => {
|
|
77
80
|
const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } =
|
|
78
81
|
issuerToEndpoints(options.issuer);
|
|
@@ -81,25 +84,30 @@ export const gitlab = (options: GitlabOptions) => {
|
|
|
81
84
|
return {
|
|
82
85
|
id: issuerId,
|
|
83
86
|
name: issuerName,
|
|
84
|
-
|
|
87
|
+
callbackPath: "/callback/gitlab",
|
|
88
|
+
createAuthorizationURL: ({
|
|
85
89
|
state,
|
|
86
90
|
scopes,
|
|
87
91
|
codeVerifier,
|
|
88
92
|
loginHint,
|
|
89
93
|
redirectURI,
|
|
94
|
+
additionalParams,
|
|
90
95
|
}) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const requestedScopes = resolveRequestedScopes(
|
|
97
|
+
options,
|
|
98
|
+
GITLAB_DEFAULT_SCOPES,
|
|
99
|
+
scopes,
|
|
100
|
+
);
|
|
101
|
+
return createAuthorizationURL({
|
|
95
102
|
id: issuerId,
|
|
96
103
|
options,
|
|
97
104
|
authorizationEndpoint,
|
|
98
|
-
scopes:
|
|
105
|
+
scopes: requestedScopes,
|
|
99
106
|
state,
|
|
100
107
|
redirectURI,
|
|
101
108
|
codeVerifier,
|
|
102
109
|
loginHint,
|
|
110
|
+
additionalParams,
|
|
103
111
|
});
|
|
104
112
|
},
|
|
105
113
|
validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => {
|
|
@@ -151,5 +159,5 @@ export const gitlab = (options: GitlabOptions) => {
|
|
|
151
159
|
};
|
|
152
160
|
},
|
|
153
161
|
options,
|
|
154
|
-
} satisfies
|
|
162
|
+
} satisfies UpstreamProvider<GitlabProfile>;
|
|
155
163
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
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
|
-
import type {
|
|
5
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
8
|
getPrimaryClientId,
|
|
9
9
|
refreshAccessToken,
|
|
10
|
+
resolveRequestedScopes,
|
|
10
11
|
validateAuthorizationCode,
|
|
11
12
|
} from "../oauth2";
|
|
12
13
|
|
|
@@ -48,15 +49,33 @@ export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
|
|
48
49
|
*/
|
|
49
50
|
display?: ("page" | "popup" | "touch" | "wap") | undefined;
|
|
50
51
|
/**
|
|
51
|
-
* The hosted domain
|
|
52
|
+
* The hosted domain (Google Workspace) the user must belong to.
|
|
53
|
+
*
|
|
54
|
+
* This is sent to Google as the `hd` authorization hint and, when set, is
|
|
55
|
+
* also enforced against the `hd` claim of the returned id token/profile.
|
|
56
|
+
* Sign-in is rejected when the claim is missing or does not match, so this
|
|
57
|
+
* can be used to restrict sign-in to a Workspace domain.
|
|
52
58
|
*/
|
|
53
59
|
hd?: string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Enable incremental authorization via Google's `include_granted_scopes`
|
|
62
|
+
* parameter. When enabled, Google reports the user's full granted scope set
|
|
63
|
+
* in the token response.
|
|
64
|
+
*
|
|
65
|
+
* @default true
|
|
66
|
+
*/
|
|
67
|
+
includeGrantedScopes?: boolean | undefined;
|
|
54
68
|
}
|
|
55
69
|
|
|
70
|
+
const GOOGLE_DEFAULT_SCOPES = ["email", "profile", "openid"];
|
|
71
|
+
|
|
56
72
|
export const google = (options: GoogleOptions) => {
|
|
57
73
|
return {
|
|
58
74
|
id: "google",
|
|
59
75
|
name: "Google",
|
|
76
|
+
callbackPath: "/callback/google",
|
|
77
|
+
grantAuthority:
|
|
78
|
+
options.includeGrantedScopes !== false ? "full-grant" : "projection",
|
|
60
79
|
async createAuthorizationURL({
|
|
61
80
|
state,
|
|
62
81
|
scopes,
|
|
@@ -64,6 +83,7 @@ export const google = (options: GoogleOptions) => {
|
|
|
64
83
|
redirectURI,
|
|
65
84
|
loginHint,
|
|
66
85
|
display,
|
|
86
|
+
additionalParams,
|
|
67
87
|
}) {
|
|
68
88
|
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
69
89
|
logger.error(
|
|
@@ -74,16 +94,16 @@ export const google = (options: GoogleOptions) => {
|
|
|
74
94
|
if (!codeVerifier) {
|
|
75
95
|
throw new BetterAuthError("codeVerifier is required for Google");
|
|
76
96
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
const requestedScopes = resolveRequestedScopes(
|
|
98
|
+
options,
|
|
99
|
+
GOOGLE_DEFAULT_SCOPES,
|
|
100
|
+
scopes,
|
|
101
|
+
);
|
|
102
|
+
return createAuthorizationURL({
|
|
83
103
|
id: "google",
|
|
84
104
|
options,
|
|
85
105
|
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
86
|
-
scopes:
|
|
106
|
+
scopes: requestedScopes,
|
|
87
107
|
state,
|
|
88
108
|
codeVerifier,
|
|
89
109
|
redirectURI,
|
|
@@ -92,11 +112,17 @@ export const google = (options: GoogleOptions) => {
|
|
|
92
112
|
display: display || options.display,
|
|
93
113
|
loginHint,
|
|
94
114
|
hd: options.hd,
|
|
95
|
-
additionalParams:
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
additionalParams:
|
|
116
|
+
options.includeGrantedScopes === false
|
|
117
|
+
? { ...(additionalParams ?? {}) }
|
|
118
|
+
: {
|
|
119
|
+
...(additionalParams ?? {}),
|
|
120
|
+
// Not caller-overridable: the emitted param must stay in
|
|
121
|
+
// lockstep with `grantAuthority` (driven by the option), or
|
|
122
|
+
// the callback would treat a non-authoritative grant as full.
|
|
123
|
+
include_granted_scopes: "true",
|
|
124
|
+
},
|
|
98
125
|
});
|
|
99
|
-
return url;
|
|
100
126
|
},
|
|
101
127
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
102
128
|
return validateAuthorizationCode({
|
|
@@ -120,37 +146,20 @@ export const google = (options: GoogleOptions) => {
|
|
|
120
146
|
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
121
147
|
});
|
|
122
148
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const publicKey = await getGooglePublicKey(kid);
|
|
139
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
140
|
-
algorithms: [jwtAlg],
|
|
141
|
-
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
142
|
-
audience: options.clientId,
|
|
143
|
-
maxTokenAge: "1h",
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return true;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
149
|
+
idToken: {
|
|
150
|
+
// https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
|
|
151
|
+
jwks: (header) => getGooglePublicKey(header.kid!),
|
|
152
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
153
|
+
audience: options.clientId,
|
|
154
|
+
maxTokenAge: "1h",
|
|
155
|
+
// Google's `hd` authorization parameter is only a UI hint and can be
|
|
156
|
+
// removed or changed by the user. When a hosted domain is configured,
|
|
157
|
+
// the `hd` claim in the verified id token is the authoritative value
|
|
158
|
+
// and must match, otherwise accounts outside the workspace domain would
|
|
159
|
+
// be accepted on the id_token sign-in path.
|
|
160
|
+
verifyClaims: options.hd
|
|
161
|
+
? (claims) => claims.hd === options.hd
|
|
162
|
+
: undefined,
|
|
154
163
|
},
|
|
155
164
|
async getUserInfo(token) {
|
|
156
165
|
if (options.getUserInfo) {
|
|
@@ -160,6 +169,18 @@ export const google = (options: GoogleOptions) => {
|
|
|
160
169
|
return null;
|
|
161
170
|
}
|
|
162
171
|
const user = decodeJwt(token.idToken) as GoogleProfile;
|
|
172
|
+
// Enforce the configured hosted domain on the callback profile path
|
|
173
|
+
// as well. The `hd` claim must be present and match, since the
|
|
174
|
+
// authorization-time `hd` hint does not restrict which account signs
|
|
175
|
+
// in.
|
|
176
|
+
if (options.hd && user.hd !== options.hd) {
|
|
177
|
+
logger.error(
|
|
178
|
+
`Google sign-in rejected: id token hosted domain (hd) "${
|
|
179
|
+
user.hd ?? "<missing>"
|
|
180
|
+
}" does not match the configured "hd" option "${options.hd}".`,
|
|
181
|
+
);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
163
184
|
const userMap = await options.mapProfileToUser?.(user);
|
|
164
185
|
return {
|
|
165
186
|
user: {
|
|
@@ -174,7 +195,7 @@ export const google = (options: GoogleOptions) => {
|
|
|
174
195
|
};
|
|
175
196
|
},
|
|
176
197
|
options,
|
|
177
|
-
} satisfies
|
|
198
|
+
} satisfies UpstreamProvider<GoogleProfile>;
|
|
178
199
|
};
|
|
179
200
|
|
|
180
201
|
export const getGooglePublicKey = async (kid: string) => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
3
3
|
import {
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
+
resolveRequestedScopes,
|
|
6
7
|
validateAuthorizationCode,
|
|
7
8
|
} from "../oauth2";
|
|
8
9
|
|
|
@@ -42,25 +43,35 @@ export interface HuggingFaceOptions
|
|
|
42
43
|
clientId: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
const HUGGINGFACE_DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
47
|
+
|
|
45
48
|
export const huggingface = (options: HuggingFaceOptions) => {
|
|
46
49
|
const tokenEndpoint = "https://huggingface.co/oauth/token";
|
|
47
50
|
return {
|
|
48
51
|
id: "huggingface",
|
|
49
52
|
name: "Hugging Face",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
callbackPath: "/callback/huggingface",
|
|
54
|
+
createAuthorizationURL({
|
|
55
|
+
state,
|
|
56
|
+
scopes,
|
|
57
|
+
codeVerifier,
|
|
58
|
+
redirectURI,
|
|
59
|
+
additionalParams,
|
|
60
|
+
}) {
|
|
61
|
+
const requestedScopes = resolveRequestedScopes(
|
|
62
|
+
options,
|
|
63
|
+
HUGGINGFACE_DEFAULT_SCOPES,
|
|
64
|
+
scopes,
|
|
65
|
+
);
|
|
56
66
|
return createAuthorizationURL({
|
|
57
67
|
id: "huggingface",
|
|
58
68
|
options,
|
|
59
69
|
authorizationEndpoint: "https://huggingface.co/oauth/authorize",
|
|
60
|
-
scopes:
|
|
70
|
+
scopes: requestedScopes,
|
|
61
71
|
state,
|
|
62
72
|
codeVerifier,
|
|
63
73
|
redirectURI,
|
|
74
|
+
additionalParams,
|
|
64
75
|
});
|
|
65
76
|
},
|
|
66
77
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -115,5 +126,5 @@ export const huggingface = (options: HuggingFaceOptions) => {
|
|
|
115
126
|
};
|
|
116
127
|
},
|
|
117
128
|
options,
|
|
118
|
-
} satisfies
|
|
129
|
+
} satisfies UpstreamProvider<HuggingFaceProfile>;
|
|
119
130
|
};
|