@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
+
import type { JWTPayload } from "jose";
|
|
2
3
|
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
3
4
|
import { logger } from "../env";
|
|
4
5
|
import { APIError, BetterAuthError } from "../error";
|
|
5
6
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
7
|
import {
|
|
7
8
|
createAuthorizationURL,
|
|
9
|
+
getPrimaryClientId,
|
|
8
10
|
refreshAccessToken,
|
|
9
11
|
validateAuthorizationCode,
|
|
10
12
|
} from "../oauth2";
|
|
@@ -37,7 +39,7 @@ export interface GoogleProfile {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
|
40
|
-
clientId: string;
|
|
42
|
+
clientId: string | string[];
|
|
41
43
|
/**
|
|
42
44
|
* The access type to use for the authorization code request
|
|
43
45
|
*/
|
|
@@ -47,11 +49,103 @@ export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
|
|
47
49
|
*/
|
|
48
50
|
display?: ("page" | "popup" | "touch" | "wap") | undefined;
|
|
49
51
|
/**
|
|
50
|
-
* 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
|
+
* Set `hd: "*"` to require any Workspace hosted-domain claim. Sign-in is
|
|
57
|
+
* rejected when the claim is missing or does not satisfy this restriction.
|
|
51
58
|
*/
|
|
52
59
|
hd?: string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Whether to send `include_granted_scopes=true` to Google's authorization
|
|
62
|
+
* endpoint, which lets new access tokens cover scopes from prior grants
|
|
63
|
+
* in addition to the ones requested for this flow. Set to `false` when
|
|
64
|
+
* each OAuth flow should request only its own scopes.
|
|
65
|
+
*
|
|
66
|
+
* Defaults to `true`.
|
|
67
|
+
*
|
|
68
|
+
* @see https://developers.google.com/identity/protocols/oauth2/web-server#incrementalAuth
|
|
69
|
+
*/
|
|
70
|
+
includeGrantedScopes?: boolean | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const GOOGLE_ID_TOKEN_MAX_AGE = "1h";
|
|
74
|
+
const GOOGLE_ID_TOKEN_ALGORITHM = "RS256";
|
|
75
|
+
type GoogleIdTokenAlgorithm = typeof GOOGLE_ID_TOKEN_ALGORITHM;
|
|
76
|
+
const GOOGLE_ID_TOKEN_ALGORITHMS: GoogleIdTokenAlgorithm[] = [
|
|
77
|
+
GOOGLE_ID_TOKEN_ALGORITHM,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function isGoogleIdTokenAlgorithm(
|
|
81
|
+
algorithm: unknown,
|
|
82
|
+
): algorithm is GoogleIdTokenAlgorithm {
|
|
83
|
+
return GOOGLE_ID_TOKEN_ALGORITHMS.includes(
|
|
84
|
+
algorithm as GoogleIdTokenAlgorithm,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface VerifyGoogleIdTokenOptions {
|
|
89
|
+
token: string;
|
|
90
|
+
audience: string | string[];
|
|
91
|
+
nonce?: string | undefined;
|
|
53
92
|
}
|
|
54
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Verifies a Google ID token against Google's issuer, audience, signature,
|
|
96
|
+
* expiry, and maximum token age.
|
|
97
|
+
*/
|
|
98
|
+
export const verifyGoogleIdToken = async ({
|
|
99
|
+
token,
|
|
100
|
+
audience,
|
|
101
|
+
nonce,
|
|
102
|
+
}: VerifyGoogleIdTokenOptions): Promise<JWTPayload | null> => {
|
|
103
|
+
try {
|
|
104
|
+
const { kid, alg } = decodeProtectedHeader(token);
|
|
105
|
+
if (!isGoogleIdTokenAlgorithm(alg)) return null;
|
|
106
|
+
|
|
107
|
+
const publicKeys = await getGooglePublicKeys(kid);
|
|
108
|
+
for (const publicKey of publicKeys) {
|
|
109
|
+
try {
|
|
110
|
+
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
111
|
+
algorithms: GOOGLE_ID_TOKEN_ALGORITHMS,
|
|
112
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
113
|
+
audience,
|
|
114
|
+
maxTokenAge: GOOGLE_ID_TOKEN_MAX_AGE,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (nonce && jwtClaims.nonce !== nonce) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return jwtClaims;
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks whether Google's verified `hd` claim satisfies the configured hosted
|
|
135
|
+
* domain restriction. `hd: "*"` accepts any Google Workspace hosted domain.
|
|
136
|
+
*/
|
|
137
|
+
export const isGoogleHostedDomainAllowed = (
|
|
138
|
+
configuredHostedDomain: string | undefined,
|
|
139
|
+
tokenHostedDomain: unknown,
|
|
140
|
+
) => {
|
|
141
|
+
if (!configuredHostedDomain) return true;
|
|
142
|
+
if (typeof tokenHostedDomain !== "string" || !tokenHostedDomain) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (configuredHostedDomain === "*") return true;
|
|
146
|
+
return tokenHostedDomain === configuredHostedDomain;
|
|
147
|
+
};
|
|
148
|
+
|
|
55
149
|
export const google = (options: GoogleOptions) => {
|
|
56
150
|
return {
|
|
57
151
|
id: "google",
|
|
@@ -63,8 +157,9 @@ export const google = (options: GoogleOptions) => {
|
|
|
63
157
|
redirectURI,
|
|
64
158
|
loginHint,
|
|
65
159
|
display,
|
|
160
|
+
additionalParams,
|
|
66
161
|
}) {
|
|
67
|
-
if (!options.clientId || !options.clientSecret) {
|
|
162
|
+
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
68
163
|
logger.error(
|
|
69
164
|
"Client Id and Client Secret is required for Google. Make sure to provide them in the options.",
|
|
70
165
|
);
|
|
@@ -92,7 +187,10 @@ export const google = (options: GoogleOptions) => {
|
|
|
92
187
|
loginHint,
|
|
93
188
|
hd: options.hd,
|
|
94
189
|
additionalParams: {
|
|
95
|
-
|
|
190
|
+
...(options.includeGrantedScopes === false
|
|
191
|
+
? {}
|
|
192
|
+
: { include_granted_scopes: "true" }),
|
|
193
|
+
...(additionalParams ?? {}),
|
|
96
194
|
},
|
|
97
195
|
});
|
|
98
196
|
return url;
|
|
@@ -119,37 +217,20 @@ export const google = (options: GoogleOptions) => {
|
|
|
119
217
|
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
120
218
|
});
|
|
121
219
|
},
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}
|
|
148
|
-
|
|
149
|
-
return true;
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
220
|
+
idToken: {
|
|
221
|
+
// https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
|
|
222
|
+
jwks: (header) => getGooglePublicKey(header.kid!),
|
|
223
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
224
|
+
audience: options.clientId,
|
|
225
|
+
maxTokenAge: GOOGLE_ID_TOKEN_MAX_AGE,
|
|
226
|
+
// Google's `hd` authorization parameter is only a UI hint and can be
|
|
227
|
+
// removed or changed by the user. When a hosted domain is configured,
|
|
228
|
+
// the `hd` claim in the verified id token is the authoritative value
|
|
229
|
+
// and must satisfy the configured restriction, otherwise accounts
|
|
230
|
+
// outside the workspace domain would be accepted on the id_token path.
|
|
231
|
+
verifyClaims: options.hd
|
|
232
|
+
? (claims) => isGoogleHostedDomainAllowed(options.hd, claims.hd)
|
|
233
|
+
: undefined,
|
|
153
234
|
},
|
|
154
235
|
async getUserInfo(token) {
|
|
155
236
|
if (options.getUserInfo) {
|
|
@@ -159,6 +240,17 @@ export const google = (options: GoogleOptions) => {
|
|
|
159
240
|
return null;
|
|
160
241
|
}
|
|
161
242
|
const user = decodeJwt(token.idToken) as GoogleProfile;
|
|
243
|
+
// Enforce the configured hosted domain on the callback profile path.
|
|
244
|
+
// The authorization-time `hd` value is only a UI hint; the verified
|
|
245
|
+
// token/profile claim is the authoritative Workspace signal.
|
|
246
|
+
if (!isGoogleHostedDomainAllowed(options.hd, user.hd)) {
|
|
247
|
+
logger.error(
|
|
248
|
+
`Google sign-in rejected: id token hosted domain (hd) "${
|
|
249
|
+
user.hd ?? "<missing>"
|
|
250
|
+
}" does not satisfy the configured "hd" option "${options.hd}".`,
|
|
251
|
+
);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
162
254
|
const userMap = await options.mapProfileToUser?.(user);
|
|
163
255
|
return {
|
|
164
256
|
user: {
|
|
@@ -177,10 +269,18 @@ export const google = (options: GoogleOptions) => {
|
|
|
177
269
|
};
|
|
178
270
|
|
|
179
271
|
export const getGooglePublicKey = async (kid: string) => {
|
|
272
|
+
const [publicKey] = await getGooglePublicKeys(kid);
|
|
273
|
+
if (!publicKey) {
|
|
274
|
+
throw new Error(`JWK with kid ${kid} not found`);
|
|
275
|
+
}
|
|
276
|
+
return publicKey;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const getGooglePublicKeys = async (kid?: string) => {
|
|
180
280
|
const { data } = await betterFetch<{
|
|
181
281
|
keys: Array<{
|
|
182
282
|
kid: string;
|
|
183
|
-
alg
|
|
283
|
+
alg?: string;
|
|
184
284
|
kty: string;
|
|
185
285
|
use: string;
|
|
186
286
|
n: string;
|
|
@@ -194,10 +294,12 @@ export const getGooglePublicKey = async (kid: string) => {
|
|
|
194
294
|
});
|
|
195
295
|
}
|
|
196
296
|
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
297
|
+
const jwks = kid ? data.keys.filter((key) => key.kid === kid) : data.keys;
|
|
298
|
+
if (!jwks.length) {
|
|
199
299
|
throw new Error(`JWK with kid ${kid} not found`);
|
|
200
300
|
}
|
|
201
301
|
|
|
202
|
-
return
|
|
302
|
+
return Promise.all(
|
|
303
|
+
jwks.map((jwk) => importJWK(jwk, GOOGLE_ID_TOKEN_ALGORITHM)),
|
|
304
|
+
);
|
|
203
305
|
};
|
|
@@ -47,7 +47,13 @@ export const huggingface = (options: HuggingFaceOptions) => {
|
|
|
47
47
|
return {
|
|
48
48
|
id: "huggingface",
|
|
49
49
|
name: "Hugging Face",
|
|
50
|
-
createAuthorizationURL({
|
|
50
|
+
createAuthorizationURL({
|
|
51
|
+
state,
|
|
52
|
+
scopes,
|
|
53
|
+
codeVerifier,
|
|
54
|
+
redirectURI,
|
|
55
|
+
additionalParams,
|
|
56
|
+
}) {
|
|
51
57
|
const _scopes = options.disableDefaultScope
|
|
52
58
|
? []
|
|
53
59
|
: ["openid", "profile", "email"];
|
|
@@ -61,6 +67,7 @@ export const huggingface = (options: HuggingFaceOptions) => {
|
|
|
61
67
|
state,
|
|
62
68
|
codeVerifier,
|
|
63
69
|
redirectURI,
|
|
70
|
+
additionalParams,
|
|
64
71
|
});
|
|
65
72
|
},
|
|
66
73
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -106,7 +106,7 @@ export const kakao = (options: KakaoOptions) => {
|
|
|
106
106
|
return {
|
|
107
107
|
id: "kakao",
|
|
108
108
|
name: "Kakao",
|
|
109
|
-
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
109
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
110
110
|
const _scopes = options.disableDefaultScope
|
|
111
111
|
? []
|
|
112
112
|
: ["account_email", "profile_image", "profile_nickname"];
|
|
@@ -119,6 +119,7 @@ export const kakao = (options: KakaoOptions) => {
|
|
|
119
119
|
scopes: _scopes,
|
|
120
120
|
state,
|
|
121
121
|
redirectURI,
|
|
122
|
+
additionalParams,
|
|
122
123
|
});
|
|
123
124
|
},
|
|
124
125
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -33,7 +33,13 @@ export const kick = (options: KickOptions) => {
|
|
|
33
33
|
return {
|
|
34
34
|
id: "kick",
|
|
35
35
|
name: "Kick",
|
|
36
|
-
createAuthorizationURL({
|
|
36
|
+
createAuthorizationURL({
|
|
37
|
+
state,
|
|
38
|
+
scopes,
|
|
39
|
+
redirectURI,
|
|
40
|
+
codeVerifier,
|
|
41
|
+
additionalParams,
|
|
42
|
+
}) {
|
|
37
43
|
const _scopes = options.disableDefaultScope ? [] : ["user:read"];
|
|
38
44
|
if (options.scope) _scopes.push(...options.scope);
|
|
39
45
|
if (scopes) _scopes.push(...scopes);
|
|
@@ -46,6 +52,7 @@ export const kick = (options: KickOptions) => {
|
|
|
46
52
|
scopes: _scopes,
|
|
47
53
|
codeVerifier,
|
|
48
54
|
state,
|
|
55
|
+
additionalParams,
|
|
49
56
|
});
|
|
50
57
|
},
|
|
51
58
|
async validateAuthorizationCode({ code, redirectURI, codeVerifier }) {
|
|
@@ -56,6 +56,7 @@ export const line = (options: LineOptions) => {
|
|
|
56
56
|
codeVerifier,
|
|
57
57
|
redirectURI,
|
|
58
58
|
loginHint,
|
|
59
|
+
additionalParams,
|
|
59
60
|
}) {
|
|
60
61
|
const _scopes = options.disableDefaultScope
|
|
61
62
|
? []
|
|
@@ -71,6 +72,7 @@ export const line = (options: LineOptions) => {
|
|
|
71
72
|
codeVerifier,
|
|
72
73
|
redirectURI,
|
|
73
74
|
loginHint,
|
|
75
|
+
additionalParams,
|
|
74
76
|
});
|
|
75
77
|
},
|
|
76
78
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -94,34 +96,30 @@ export const line = (options: LineOptions) => {
|
|
|
94
96
|
tokenEndpoint,
|
|
95
97
|
});
|
|
96
98
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
method: "POST",
|
|
112
|
-
headers: {
|
|
113
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
99
|
+
idToken: {
|
|
100
|
+
verify: async (token, nonce) => {
|
|
101
|
+
const body = new URLSearchParams();
|
|
102
|
+
body.set("id_token", token);
|
|
103
|
+
body.set("client_id", options.clientId);
|
|
104
|
+
if (nonce) body.set("nonce", nonce);
|
|
105
|
+
const { data, error } = await betterFetch<LineIdTokenPayload>(
|
|
106
|
+
verifyIdTokenEndpoint,
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
111
|
+
},
|
|
112
|
+
body,
|
|
114
113
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return true;
|
|
114
|
+
);
|
|
115
|
+
if (error || !data) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// aud must match clientId; nonce (if provided) must also match nonce
|
|
119
|
+
if (data.aud !== options.clientId) return false;
|
|
120
|
+
if (data.nonce && data.nonce !== nonce) return false;
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
125
123
|
},
|
|
126
124
|
async getUserInfo(token) {
|
|
127
125
|
if (options.getUserInfo) {
|
|
@@ -31,7 +31,13 @@ export const linear = (options: LinearOptions) => {
|
|
|
31
31
|
return {
|
|
32
32
|
id: "linear",
|
|
33
33
|
name: "Linear",
|
|
34
|
-
createAuthorizationURL({
|
|
34
|
+
createAuthorizationURL({
|
|
35
|
+
state,
|
|
36
|
+
scopes,
|
|
37
|
+
loginHint,
|
|
38
|
+
redirectURI,
|
|
39
|
+
additionalParams,
|
|
40
|
+
}) {
|
|
35
41
|
const _scopes = options.disableDefaultScope ? [] : ["read"];
|
|
36
42
|
if (options.scope) _scopes.push(...options.scope);
|
|
37
43
|
if (scopes) _scopes.push(...scopes);
|
|
@@ -43,6 +49,7 @@ export const linear = (options: LinearOptions) => {
|
|
|
43
49
|
state,
|
|
44
50
|
redirectURI,
|
|
45
51
|
loginHint,
|
|
52
|
+
additionalParams,
|
|
46
53
|
});
|
|
47
54
|
},
|
|
48
55
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -16,8 +16,8 @@ export interface LinkedInProfile {
|
|
|
16
16
|
country: string;
|
|
17
17
|
language: string;
|
|
18
18
|
};
|
|
19
|
-
email
|
|
20
|
-
email_verified
|
|
19
|
+
email?: string;
|
|
20
|
+
email_verified?: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface LinkedInOptions extends ProviderOptions<LinkedInProfile> {
|
|
@@ -37,6 +37,7 @@ export const linkedin = (options: LinkedInOptions) => {
|
|
|
37
37
|
scopes,
|
|
38
38
|
redirectURI,
|
|
39
39
|
loginHint,
|
|
40
|
+
additionalParams,
|
|
40
41
|
}) => {
|
|
41
42
|
const _scopes = options.disableDefaultScope
|
|
42
43
|
? []
|
|
@@ -51,6 +52,7 @@ export const linkedin = (options: LinkedInOptions) => {
|
|
|
51
52
|
state,
|
|
52
53
|
loginHint,
|
|
53
54
|
redirectURI,
|
|
55
|
+
additionalParams,
|
|
54
56
|
});
|
|
55
57
|
},
|
|
56
58
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
@@ -98,7 +100,7 @@ export const linkedin = (options: LinkedInOptions) => {
|
|
|
98
100
|
id: profile.sub,
|
|
99
101
|
name: profile.name,
|
|
100
102
|
email: profile.email,
|
|
101
|
-
emailVerified: profile.email_verified
|
|
103
|
+
emailVerified: profile.email_verified ?? false,
|
|
102
104
|
image: profile.picture,
|
|
103
105
|
...userMap,
|
|
104
106
|
},
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { base64 } from "@better-auth/utils/base64";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import { decodeJwt,
|
|
3
|
+
import { decodeJwt, importJWK } from "jose";
|
|
4
4
|
import { logger } from "../env";
|
|
5
|
-
import { APIError } from "../error";
|
|
6
|
-
import type {
|
|
5
|
+
import { APIError, BetterAuthError } from "../error";
|
|
6
|
+
import type {
|
|
7
|
+
ClientAssertionGetter,
|
|
8
|
+
OAuthProvider,
|
|
9
|
+
ProviderOptions,
|
|
10
|
+
TokenEndpointAuth,
|
|
11
|
+
} from "../oauth2";
|
|
7
12
|
import {
|
|
8
13
|
createAuthorizationURL,
|
|
14
|
+
getPrimaryClientId,
|
|
9
15
|
refreshAccessToken,
|
|
10
16
|
validateAuthorizationCode,
|
|
11
17
|
} from "../oauth2";
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Microsoft's fixed tenant id for personal (consumer) Microsoft accounts. Every
|
|
21
|
+
* personal-account token carries it as the `tid` claim, so it distinguishes the
|
|
22
|
+
* consumer account class from work/school tenants.
|
|
23
|
+
* @see https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
|
|
24
|
+
*/
|
|
25
|
+
const MICROSOFT_CONSUMER_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad";
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference)
|
|
15
29
|
*/
|
|
@@ -35,7 +49,7 @@ export interface MicrosoftEntraIDProfile extends Record<string, any> {
|
|
|
35
49
|
/** The primary username that represents the user */
|
|
36
50
|
preferred_username: string;
|
|
37
51
|
/** User's email address */
|
|
38
|
-
email
|
|
52
|
+
email?: string;
|
|
39
53
|
/** Human-readable value that identifies the subject of the token */
|
|
40
54
|
name: string;
|
|
41
55
|
/** Matches the parameter included in the original authorize request */
|
|
@@ -116,39 +130,73 @@ export interface MicrosoftEntraIDProfile extends Record<string, any> {
|
|
|
116
130
|
|
|
117
131
|
export interface MicrosoftOptions
|
|
118
132
|
extends ProviderOptions<MicrosoftEntraIDProfile> {
|
|
119
|
-
clientId: string;
|
|
133
|
+
clientId: string | string[];
|
|
120
134
|
/**
|
|
121
135
|
* The tenant ID of the Microsoft account
|
|
122
136
|
* @default "common"
|
|
123
137
|
*/
|
|
124
|
-
tenantId?: string
|
|
138
|
+
tenantId?: string;
|
|
125
139
|
/**
|
|
126
140
|
* The authentication authority URL. Use the default "https://login.microsoftonline.com" for standard Entra ID or "https://<tenant-id>.ciamlogin.com" for CIAM scenarios.
|
|
127
141
|
* @default "https://login.microsoftonline.com"
|
|
128
142
|
*/
|
|
129
|
-
authority?: string
|
|
143
|
+
authority?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Function that returns a JWT client assertion for token endpoint authentication.
|
|
146
|
+
*
|
|
147
|
+
* Use this instead of `clientSecret` when your Microsoft Entra ID app is
|
|
148
|
+
* configured for client authentication with assertions (private_key_jwt or
|
|
149
|
+
* workload identity federation).
|
|
150
|
+
*/
|
|
151
|
+
clientAssertion?: ClientAssertionGetter;
|
|
130
152
|
/**
|
|
131
153
|
* The size of the profile photo
|
|
132
154
|
* @default 48
|
|
133
155
|
*/
|
|
134
|
-
profilePhotoSize?:
|
|
135
|
-
| (48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648)
|
|
136
|
-
| undefined;
|
|
156
|
+
profilePhotoSize?: 48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648;
|
|
137
157
|
/**
|
|
138
158
|
* Disable profile photo
|
|
139
159
|
*/
|
|
140
|
-
disableProfilePhoto?: boolean
|
|
160
|
+
disableProfilePhoto?: boolean;
|
|
141
161
|
}
|
|
142
162
|
|
|
143
163
|
export const microsoft = (options: MicrosoftOptions) => {
|
|
144
164
|
const tenant = options.tenantId || "common";
|
|
145
|
-
|
|
165
|
+
// Trim any trailing slash so endpoint URLs and the issuer comparison below
|
|
166
|
+
// never produce a double slash (e.g. a configured `https://host/` would make
|
|
167
|
+
// the expected issuer `https://host//<tid>/v2.0` and reject every token). A
|
|
168
|
+
// loop avoids a trailing-slash regex, which is a polynomial-ReDoS shape.
|
|
169
|
+
let authority = options.authority || "https://login.microsoftonline.com";
|
|
170
|
+
while (authority.endsWith("/")) {
|
|
171
|
+
authority = authority.slice(0, -1);
|
|
172
|
+
}
|
|
146
173
|
const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;
|
|
147
174
|
const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
|
|
175
|
+
if (options.clientSecret && options.clientAssertion) {
|
|
176
|
+
throw new BetterAuthError(
|
|
177
|
+
"Microsoft Entra ID clientAssertion cannot be combined with clientSecret",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const tokenEndpointAuth: TokenEndpointAuth | undefined =
|
|
181
|
+
options.clientAssertion
|
|
182
|
+
? {
|
|
183
|
+
method: "private_key_jwt",
|
|
184
|
+
getClientAssertion: options.clientAssertion,
|
|
185
|
+
}
|
|
186
|
+
: undefined;
|
|
148
187
|
return {
|
|
149
188
|
id: "microsoft",
|
|
150
189
|
name: "Microsoft EntraID",
|
|
151
190
|
createAuthorizationURL(data) {
|
|
191
|
+
// Microsoft Entra supports public clients (SPA / native apps with
|
|
192
|
+
// PKCE only), so clientSecret is intentionally not required here.
|
|
193
|
+
// See https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
|
194
|
+
if (!getPrimaryClientId(options.clientId)) {
|
|
195
|
+
logger.error(
|
|
196
|
+
"Client Id is required for Microsoft Entra ID. Make sure to provide it in the options.",
|
|
197
|
+
);
|
|
198
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
199
|
+
}
|
|
152
200
|
const scopes = options.disableDefaultScope
|
|
153
201
|
? []
|
|
154
202
|
: ["openid", "profile", "email", "User.Read", "offline_access"];
|
|
@@ -164,6 +212,7 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
164
212
|
redirectURI: data.redirectURI,
|
|
165
213
|
prompt: options.prompt,
|
|
166
214
|
loginHint: data.loginHint,
|
|
215
|
+
additionalParams: data.additionalParams,
|
|
167
216
|
});
|
|
168
217
|
},
|
|
169
218
|
validateAuthorizationCode({ code, codeVerifier, redirectURI }) {
|
|
@@ -173,57 +222,52 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
173
222
|
redirectURI,
|
|
174
223
|
options,
|
|
175
224
|
tokenEndpoint,
|
|
225
|
+
tokenEndpointAuth,
|
|
176
226
|
});
|
|
177
227
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
228
|
+
idToken: {
|
|
229
|
+
jwks: (header) => getMicrosoftPublicKey(header.kid!, tenant, authority),
|
|
230
|
+
audience: options.clientId,
|
|
231
|
+
maxTokenAge: "1h",
|
|
232
|
+
/**
|
|
233
|
+
* Issuer varies per tenant for multi-tenant endpoints, so only validate it for
|
|
234
|
+
* specific tenants.
|
|
235
|
+
* @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
|
|
236
|
+
*/
|
|
237
|
+
issuer:
|
|
238
|
+
tenant !== "common" &&
|
|
239
|
+
tenant !== "organizations" &&
|
|
240
|
+
tenant !== "consumers"
|
|
241
|
+
? `${authority}/${tenant}/v2.0`
|
|
242
|
+
: undefined,
|
|
243
|
+
/**
|
|
244
|
+
* The multi-tenant endpoints (common/organizations/consumers) skip the
|
|
245
|
+
* issuer check above because the issuer varies per tenant, and the
|
|
246
|
+
* organizations and consumers JWKS sets overlap. Enforce the tenant
|
|
247
|
+
* binding explicitly so a token from a disallowed account class cannot
|
|
248
|
+
* pass: the issuer must name the token's own tenant, and the account
|
|
249
|
+
* class must match the configured restriction.
|
|
250
|
+
* @see https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
|
|
251
|
+
*/
|
|
252
|
+
verifyClaims: (claims) => {
|
|
253
|
+
const tid = claims.tid;
|
|
205
254
|
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
tenant !== "consumers"
|
|
255
|
+
typeof tid !== "string" ||
|
|
256
|
+
claims.iss !== `${authority}/${tid}/v2.0`
|
|
209
257
|
) {
|
|
210
|
-
|
|
258
|
+
return false;
|
|
211
259
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
tenant === "organizations" &&
|
|
262
|
+
tid === MICROSOFT_CONSUMER_TENANT_ID
|
|
263
|
+
) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (tenant === "consumers" && tid !== MICROSOFT_CONSUMER_TENANT_ID) {
|
|
219
267
|
return false;
|
|
220
268
|
}
|
|
221
|
-
|
|
222
269
|
return true;
|
|
223
|
-
}
|
|
224
|
-
logger.error("Failed to verify ID token:", error);
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
270
|
+
},
|
|
227
271
|
},
|
|
228
272
|
async getUserInfo(token) {
|
|
229
273
|
if (options.getUserInfo) {
|
|
@@ -303,6 +347,7 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
303
347
|
scope: scopes.join(" "), // Include the scopes in request to microsoft
|
|
304
348
|
},
|
|
305
349
|
tokenEndpoint,
|
|
350
|
+
tokenEndpointAuth,
|
|
306
351
|
});
|
|
307
352
|
},
|
|
308
353
|
options,
|