@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
package/src/oauth2/verify.ts
CHANGED
|
@@ -9,13 +9,38 @@ import type {
|
|
|
9
9
|
import {
|
|
10
10
|
createLocalJWKSet,
|
|
11
11
|
decodeProtectedHeader,
|
|
12
|
+
errors as joseErrors,
|
|
12
13
|
jwtVerify,
|
|
13
14
|
UnsecuredJWT,
|
|
14
15
|
} from "jose";
|
|
15
16
|
import { logger } from "../env";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const joseInfrastructureErrorCodes = new Set([
|
|
19
|
+
joseErrors.JWKSTimeout.code,
|
|
20
|
+
joseErrors.JWKSInvalid.code,
|
|
21
|
+
joseErrors.JWKSMultipleMatchingKeys.code,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function isJoseInfrastructureError(error: joseErrors.JOSEError) {
|
|
25
|
+
return joseInfrastructureErrorCodes.has(error.code);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface JwksCacheEntry {
|
|
29
|
+
jwks: JSONWebKeySet;
|
|
30
|
+
fetchedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const jwksCache = new Map<
|
|
34
|
+
string | (() => Promise<JSONWebKeySet | undefined>),
|
|
35
|
+
JwksCacheEntry
|
|
36
|
+
>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How long a cached JWKS is trusted before it is refetched
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
19
44
|
|
|
20
45
|
export interface VerifyAccessTokenRemote {
|
|
21
46
|
/** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */
|
|
@@ -30,6 +55,20 @@ export interface VerifyAccessTokenRemote {
|
|
|
30
55
|
* is also still active.
|
|
31
56
|
*/
|
|
32
57
|
force?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Accept introspection responses that omit the `aud` claim even when a
|
|
60
|
+
* required `audience` is configured in `verifyOptions`.
|
|
61
|
+
*
|
|
62
|
+
* By default verification fails closed: if you configure an `audience` and
|
|
63
|
+
* the introspection response has no `aud` (or a mismatching one), the token
|
|
64
|
+
* is rejected. Some authorization servers legitimately omit `aud` from
|
|
65
|
+
* introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
|
|
66
|
+
* this if you trust the issuer to bind the token to this resource through
|
|
67
|
+
* another mechanism, as it skips the audience check in that case.
|
|
68
|
+
*
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
allowMissingAudience?: boolean;
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
/**
|
|
@@ -82,11 +121,24 @@ export async function getJwks(
|
|
|
82
121
|
throw new Error(error as unknown as string);
|
|
83
122
|
}
|
|
84
123
|
|
|
85
|
-
if (!jwtHeaders.kid)
|
|
124
|
+
if (!jwtHeaders.kid) {
|
|
125
|
+
throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
|
|
126
|
+
}
|
|
127
|
+
const kid = jwtHeaders.kid;
|
|
128
|
+
|
|
129
|
+
const cacheKey = opts.jwksFetch;
|
|
130
|
+
const cached = jwksCache.get(cacheKey);
|
|
131
|
+
const isFresh = cached
|
|
132
|
+
? Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS
|
|
133
|
+
: false;
|
|
134
|
+
const hasKid = cached?.jwks.keys.some((jwk) => jwk.kid === kid) ?? false;
|
|
86
135
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
136
|
+
// Refetch when this source has no cached set, the cached set has expired, or
|
|
137
|
+
// it does not contain the token's kid (e.g. a newly rotated-in key). The
|
|
138
|
+
// cache is scoped to `cacheKey`, so a token is only ever matched against the
|
|
139
|
+
// key set published by its own source.
|
|
140
|
+
if (!cached || !isFresh || !hasKid) {
|
|
141
|
+
const jwks =
|
|
90
142
|
typeof opts.jwksFetch === "string"
|
|
91
143
|
? await betterFetch<JSONWebKeySet>(opts.jwksFetch, {
|
|
92
144
|
headers: {
|
|
@@ -101,9 +153,11 @@ export async function getJwks(
|
|
|
101
153
|
})
|
|
102
154
|
: await opts.jwksFetch();
|
|
103
155
|
if (!jwks) throw new Error("No jwks found");
|
|
156
|
+
jwksCache.set(cacheKey, { jwks, fetchedAt: Date.now() });
|
|
157
|
+
return jwks;
|
|
104
158
|
}
|
|
105
159
|
|
|
106
|
-
return jwks;
|
|
160
|
+
return cached.jwks;
|
|
107
161
|
}
|
|
108
162
|
|
|
109
163
|
/**
|
|
@@ -137,13 +191,16 @@ export async function verifyAccessToken(
|
|
|
137
191
|
if (error instanceof Error) {
|
|
138
192
|
if (error.name === "TypeError" || error.name === "JWSInvalid") {
|
|
139
193
|
// likely an opaque token (continue)
|
|
140
|
-
} else if (error
|
|
194
|
+
} else if (error instanceof joseErrors.JWTExpired) {
|
|
141
195
|
throw new APIError("UNAUTHORIZED", {
|
|
142
196
|
message: "token expired",
|
|
143
197
|
});
|
|
144
|
-
} else if (error
|
|
198
|
+
} else if (error instanceof joseErrors.JOSEError) {
|
|
199
|
+
if (isJoseInfrastructureError(error)) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
145
202
|
throw new APIError("UNAUTHORIZED", {
|
|
146
|
-
message: "token
|
|
203
|
+
message: "invalid access token",
|
|
147
204
|
});
|
|
148
205
|
} else {
|
|
149
206
|
throw error;
|
|
@@ -185,13 +242,23 @@ export async function verifyAccessToken(
|
|
|
185
242
|
throw new APIError("UNAUTHORIZED", {
|
|
186
243
|
message: "token inactive",
|
|
187
244
|
});
|
|
188
|
-
// Verifies payload using verify options (token valid through introspect)
|
|
245
|
+
// Verifies payload using verify options (token valid through introspect).
|
|
246
|
+
// Audience is enforced by default: when `verifyOptions.audience` is set
|
|
247
|
+
// but the introspection response omits `aud` (or it mismatches),
|
|
248
|
+
// `UnsecuredJWT.decode` throws and the token is rejected. Otherwise a
|
|
249
|
+
// token issued for a different resource/client on the same issuer would
|
|
250
|
+
// also pass. Only drop the audience check when the caller has explicitly
|
|
251
|
+
// opted in via `remoteVerify.allowMissingAudience`.
|
|
189
252
|
try {
|
|
190
253
|
const unsecuredJwt = new UnsecuredJWT(introspect).encode();
|
|
191
|
-
const { audience: _audience, ...
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
const { audience: _audience, ...verifyOptionsNoAudience } =
|
|
255
|
+
opts.verifyOptions;
|
|
256
|
+
const skipAudience =
|
|
257
|
+
!introspect.aud && opts.remoteVerify.allowMissingAudience === true;
|
|
258
|
+
const verify = UnsecuredJWT.decode(
|
|
259
|
+
unsecuredJwt,
|
|
260
|
+
skipAudience ? verifyOptionsNoAudience : opts.verifyOptions,
|
|
261
|
+
);
|
|
195
262
|
payload = verify.payload;
|
|
196
263
|
} catch (error) {
|
|
197
264
|
throw new Error(error as unknown as string);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
|
|
3
|
-
import { decodeJwt,
|
|
3
|
+
import { decodeJwt, importJWK } from "jose";
|
|
4
4
|
import { logger } from "../env";
|
|
5
5
|
import { APIError, BetterAuthError } from "../error";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
7
7
|
import {
|
|
8
8
|
createAuthorizationURL,
|
|
9
9
|
getPrimaryClientId,
|
|
10
10
|
refreshAccessToken,
|
|
11
|
+
resolveRequestedScopes,
|
|
11
12
|
validateAuthorizationCode,
|
|
12
13
|
} from "../oauth2";
|
|
13
14
|
export interface AppleProfile {
|
|
@@ -77,32 +78,42 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
|
|
|
77
78
|
audience?: (string | string[]) | undefined;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
const APPLE_DEFAULT_SCOPES = ["email", "name"];
|
|
82
|
+
|
|
80
83
|
export const apple = (options: AppleOptions) => {
|
|
81
84
|
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
82
85
|
return {
|
|
83
86
|
id: "apple",
|
|
84
87
|
name: "Apple",
|
|
85
|
-
|
|
88
|
+
callbackPath: "/callback/apple",
|
|
89
|
+
async createAuthorizationURL({
|
|
90
|
+
state,
|
|
91
|
+
scopes,
|
|
92
|
+
redirectURI,
|
|
93
|
+
additionalParams,
|
|
94
|
+
}) {
|
|
86
95
|
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
87
96
|
logger.error(
|
|
88
97
|
"Client ID and client secret are required for Apple. Make sure to provide them in the options.",
|
|
89
98
|
);
|
|
90
99
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
91
100
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
const requestedScopes = resolveRequestedScopes(
|
|
102
|
+
options,
|
|
103
|
+
APPLE_DEFAULT_SCOPES,
|
|
104
|
+
scopes,
|
|
105
|
+
);
|
|
106
|
+
return createAuthorizationURL({
|
|
96
107
|
id: "apple",
|
|
97
108
|
options,
|
|
98
109
|
authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
|
|
99
|
-
scopes:
|
|
110
|
+
scopes: requestedScopes,
|
|
100
111
|
state,
|
|
101
112
|
redirectURI,
|
|
102
113
|
responseMode: "form_post",
|
|
103
114
|
responseType: "code id_token",
|
|
115
|
+
additionalParams,
|
|
104
116
|
});
|
|
105
|
-
return url;
|
|
106
117
|
},
|
|
107
118
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
108
119
|
return validateAuthorizationCode({
|
|
@@ -113,41 +124,17 @@ export const apple = (options: AppleOptions) => {
|
|
|
113
124
|
tokenEndpoint,
|
|
114
125
|
});
|
|
115
126
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const publicKey = await getApplePublicKey(kid);
|
|
128
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
129
|
-
algorithms: [jwtAlg],
|
|
130
|
-
issuer: "https://appleid.apple.com",
|
|
131
|
-
audience:
|
|
132
|
-
options.audience && options.audience.length
|
|
133
|
-
? options.audience
|
|
134
|
-
: options.appBundleIdentifier
|
|
135
|
-
? options.appBundleIdentifier
|
|
136
|
-
: options.clientId,
|
|
137
|
-
maxTokenAge: "1h",
|
|
138
|
-
});
|
|
139
|
-
["email_verified", "is_private_email"].forEach((field) => {
|
|
140
|
-
if (jwtClaims[field] !== undefined) {
|
|
141
|
-
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
return !!jwtClaims;
|
|
148
|
-
} catch {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
127
|
+
idToken: {
|
|
128
|
+
jwks: (header) => getApplePublicKey(header.kid!),
|
|
129
|
+
issuer: "https://appleid.apple.com",
|
|
130
|
+
audience:
|
|
131
|
+
options.audience && options.audience.length
|
|
132
|
+
? options.audience
|
|
133
|
+
: options.appBundleIdentifier
|
|
134
|
+
? options.appBundleIdentifier
|
|
135
|
+
: options.clientId,
|
|
136
|
+
maxTokenAge: "1h",
|
|
137
|
+
nonceComparison: "exact-or-sha256",
|
|
151
138
|
},
|
|
152
139
|
refreshAccessToken: options.refreshAccessToken
|
|
153
140
|
? options.refreshAccessToken
|
|
@@ -202,7 +189,7 @@ export const apple = (options: AppleOptions) => {
|
|
|
202
189
|
};
|
|
203
190
|
},
|
|
204
191
|
options,
|
|
205
|
-
} satisfies
|
|
192
|
+
} satisfies UpstreamProvider<AppleProfile>;
|
|
206
193
|
};
|
|
207
194
|
|
|
208
195
|
export const getApplePublicKey = async (kid: string) => {
|
|
@@ -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
|
|
|
@@ -29,13 +30,22 @@ export interface AtlassianOptions extends ProviderOptions<AtlassianProfile> {
|
|
|
29
30
|
clientId: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const ATLASSIAN_DEFAULT_SCOPES = ["read:jira-user", "offline_access"];
|
|
34
|
+
|
|
32
35
|
export const atlassian = (options: AtlassianOptions) => {
|
|
33
36
|
const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
|
|
34
37
|
return {
|
|
35
38
|
id: "atlassian",
|
|
36
39
|
name: "Atlassian",
|
|
40
|
+
callbackPath: "/callback/atlassian",
|
|
37
41
|
|
|
38
|
-
async createAuthorizationURL({
|
|
42
|
+
async createAuthorizationURL({
|
|
43
|
+
state,
|
|
44
|
+
scopes,
|
|
45
|
+
codeVerifier,
|
|
46
|
+
redirectURI,
|
|
47
|
+
additionalParams,
|
|
48
|
+
}) {
|
|
39
49
|
if (!options.clientId || !options.clientSecret) {
|
|
40
50
|
logger.error("Client Id and Secret are required for Atlassian");
|
|
41
51
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
@@ -44,21 +54,22 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
44
54
|
throw new BetterAuthError("codeVerifier is required for Atlassian");
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const requestedScopes = resolveRequestedScopes(
|
|
58
|
+
options,
|
|
59
|
+
ATLASSIAN_DEFAULT_SCOPES,
|
|
60
|
+
scopes,
|
|
61
|
+
);
|
|
52
62
|
|
|
53
63
|
return createAuthorizationURL({
|
|
54
64
|
id: "atlassian",
|
|
55
65
|
options,
|
|
56
66
|
authorizationEndpoint: "https://auth.atlassian.com/authorize",
|
|
57
|
-
scopes:
|
|
67
|
+
scopes: requestedScopes,
|
|
58
68
|
state,
|
|
59
69
|
codeVerifier,
|
|
60
70
|
redirectURI,
|
|
61
71
|
additionalParams: {
|
|
72
|
+
...(additionalParams ?? {}),
|
|
62
73
|
audience: "api.atlassian.com",
|
|
63
74
|
},
|
|
64
75
|
prompt: options.prompt,
|
|
@@ -129,5 +140,5 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
129
140
|
},
|
|
130
141
|
|
|
131
142
|
options,
|
|
132
|
-
} satisfies
|
|
143
|
+
} satisfies UpstreamProvider<AtlassianProfile>;
|
|
133
144
|
};
|
|
@@ -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
|
|
|
@@ -42,8 +43,23 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
|
42
43
|
region: string;
|
|
43
44
|
userPoolId: string;
|
|
44
45
|
requireClientSecret?: boolean | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Skip the Cognito hosted-UI identity-provider picker by preselecting an
|
|
48
|
+
* IdP (maps to the `identity_provider` query parameter on the authorize
|
|
49
|
+
* request). Accepts `"COGNITO"`, a SAML/OIDC provider name configured on
|
|
50
|
+
* the User Pool, or one of the social providers (`"Google"`, `"Facebook"`,
|
|
51
|
+
* `"LoginWithAmazon"`, `"SignInWithApple"`).
|
|
52
|
+
*
|
|
53
|
+
* Per-request overrides via `signIn.social({ additionalParams: { identity_provider } })`
|
|
54
|
+
* take precedence over this value.
|
|
55
|
+
*
|
|
56
|
+
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
|
|
57
|
+
*/
|
|
58
|
+
identityProvider?: string | undefined;
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
62
|
+
|
|
47
63
|
export const cognito = (options: CognitoOptions) => {
|
|
48
64
|
if (!options.domain || !options.region || !options.userPoolId) {
|
|
49
65
|
logger.error(
|
|
@@ -60,7 +76,14 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
60
76
|
return {
|
|
61
77
|
id: "cognito",
|
|
62
78
|
name: "Cognito",
|
|
63
|
-
|
|
79
|
+
callbackPath: "/callback/cognito",
|
|
80
|
+
async createAuthorizationURL({
|
|
81
|
+
state,
|
|
82
|
+
scopes,
|
|
83
|
+
codeVerifier,
|
|
84
|
+
redirectURI,
|
|
85
|
+
additionalParams,
|
|
86
|
+
}) {
|
|
64
87
|
if (!getPrimaryClientId(options.clientId)) {
|
|
65
88
|
logger.error(
|
|
66
89
|
"ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
|
|
@@ -74,23 +97,29 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
74
97
|
);
|
|
75
98
|
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
|
|
76
99
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
const requestedScopes = resolveRequestedScopes(
|
|
101
|
+
options,
|
|
102
|
+
COGNITO_DEFAULT_SCOPES,
|
|
103
|
+
scopes,
|
|
104
|
+
);
|
|
82
105
|
|
|
83
|
-
const url = await createAuthorizationURL({
|
|
106
|
+
const { url } = await createAuthorizationURL({
|
|
84
107
|
id: "cognito",
|
|
85
108
|
options: {
|
|
86
109
|
...options,
|
|
87
110
|
},
|
|
88
111
|
authorizationEndpoint,
|
|
89
|
-
scopes:
|
|
112
|
+
scopes: requestedScopes,
|
|
90
113
|
state,
|
|
91
114
|
codeVerifier,
|
|
92
115
|
redirectURI,
|
|
93
116
|
prompt: options.prompt,
|
|
117
|
+
additionalParams: {
|
|
118
|
+
...(options.identityProvider
|
|
119
|
+
? { identity_provider: options.identityProvider }
|
|
120
|
+
: {}),
|
|
121
|
+
...(additionalParams ?? {}),
|
|
122
|
+
},
|
|
94
123
|
});
|
|
95
124
|
// AWS Cognito requires scopes to be encoded with %20 instead of +
|
|
96
125
|
// URLSearchParams encodes spaces as + by default, so we need to fix this
|
|
@@ -101,9 +130,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
101
130
|
// Manually append the scope with proper encoding to the URL
|
|
102
131
|
const urlString = url.toString();
|
|
103
132
|
const separator = urlString.includes("?") ? "&" : "?";
|
|
104
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
url: new URL(`${urlString}${separator}scope=${encodedScope}`),
|
|
135
|
+
requestedScopes,
|
|
136
|
+
};
|
|
105
137
|
}
|
|
106
|
-
return url;
|
|
138
|
+
return { url, requestedScopes };
|
|
107
139
|
},
|
|
108
140
|
|
|
109
141
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -130,41 +162,12 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
130
162
|
});
|
|
131
163
|
},
|
|
132
164
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
const decodedHeader = decodeProtectedHeader(token);
|
|
143
|
-
const { kid, alg: jwtAlg } = decodedHeader;
|
|
144
|
-
if (!kid || !jwtAlg) return false;
|
|
145
|
-
|
|
146
|
-
const publicKey = await getCognitoPublicKey(
|
|
147
|
-
kid,
|
|
148
|
-
options.region,
|
|
149
|
-
options.userPoolId,
|
|
150
|
-
);
|
|
151
|
-
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
|
|
152
|
-
|
|
153
|
-
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
|
|
154
|
-
algorithms: [jwtAlg],
|
|
155
|
-
issuer: expectedIssuer,
|
|
156
|
-
audience: options.clientId,
|
|
157
|
-
maxTokenAge: "1h",
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (nonce && jwtClaims.nonce !== nonce) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
return true;
|
|
164
|
-
} catch (error) {
|
|
165
|
-
logger.error("Failed to verify ID token:", error);
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
165
|
+
idToken: {
|
|
166
|
+
jwks: (header) =>
|
|
167
|
+
getCognitoPublicKey(header.kid!, options.region, options.userPoolId),
|
|
168
|
+
issuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
|
|
169
|
+
audience: options.clientId,
|
|
170
|
+
maxTokenAge: "1h",
|
|
168
171
|
},
|
|
169
172
|
|
|
170
173
|
async getUserInfo(token) {
|
|
@@ -240,7 +243,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
240
243
|
},
|
|
241
244
|
|
|
242
245
|
options,
|
|
243
|
-
} satisfies
|
|
246
|
+
} satisfies UpstreamProvider<CognitoProfile>;
|
|
244
247
|
};
|
|
245
248
|
|
|
246
249
|
export const getCognitoPublicKey = async (
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "../oauth2";
|
|
3
|
+
import {
|
|
4
|
+
createAuthorizationURL,
|
|
5
|
+
refreshAccessToken,
|
|
6
|
+
resolveRequestedScopes,
|
|
7
|
+
validateAuthorizationCode,
|
|
8
|
+
} from "../oauth2";
|
|
4
9
|
export interface DiscordProfile extends Record<string, any> {
|
|
5
10
|
/** the user's id (i.e. the numerical snowflake) */
|
|
6
11
|
id: string;
|
|
@@ -79,31 +84,41 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
|
|
|
79
84
|
permissions?: number | undefined;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
|
|
88
|
+
|
|
82
89
|
export const discord = (options: DiscordOptions) => {
|
|
83
90
|
const tokenEndpoint = "https://discord.com/api/oauth2/token";
|
|
84
91
|
return {
|
|
85
92
|
id: "discord",
|
|
86
93
|
name: "Discord",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"+",
|
|
99
|
-
)}&response_type=code&client_id=${
|
|
100
|
-
options.clientId
|
|
101
|
-
}&redirect_uri=${encodeURIComponent(
|
|
102
|
-
options.redirectURI || redirectURI,
|
|
103
|
-
)}&state=${state}&prompt=${
|
|
104
|
-
options.prompt || "none"
|
|
105
|
-
}${permissionsParam}`,
|
|
94
|
+
callbackPath: "/callback/discord",
|
|
95
|
+
async createAuthorizationURL({
|
|
96
|
+
state,
|
|
97
|
+
scopes,
|
|
98
|
+
redirectURI,
|
|
99
|
+
additionalParams,
|
|
100
|
+
}) {
|
|
101
|
+
const requestedScopes = resolveRequestedScopes(
|
|
102
|
+
options,
|
|
103
|
+
DISCORD_DEFAULT_SCOPES,
|
|
104
|
+
scopes,
|
|
106
105
|
);
|
|
106
|
+
const hasBotScope = requestedScopes.includes("bot");
|
|
107
|
+
return createAuthorizationURL({
|
|
108
|
+
id: "discord",
|
|
109
|
+
options,
|
|
110
|
+
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
|
|
111
|
+
scopes: requestedScopes,
|
|
112
|
+
state,
|
|
113
|
+
redirectURI,
|
|
114
|
+
prompt: options.prompt || "none",
|
|
115
|
+
additionalParams: {
|
|
116
|
+
...(hasBotScope && options.permissions !== undefined
|
|
117
|
+
? { permissions: String(options.permissions) }
|
|
118
|
+
: {}),
|
|
119
|
+
...(additionalParams ?? {}),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
107
122
|
},
|
|
108
123
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
109
124
|
return validateAuthorizationCode({
|
|
@@ -166,5 +181,5 @@ export const discord = (options: DiscordOptions) => {
|
|
|
166
181
|
};
|
|
167
182
|
},
|
|
168
183
|
options,
|
|
169
|
-
} satisfies
|
|
184
|
+
} satisfies UpstreamProvider<DiscordProfile>;
|
|
170
185
|
};
|
|
@@ -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
|
|
|
@@ -25,34 +26,41 @@ export interface DropboxOptions extends ProviderOptions<DropboxProfile> {
|
|
|
25
26
|
accessType?: ("offline" | "online" | "legacy") | undefined;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
const DROPBOX_DEFAULT_SCOPES = ["account_info.read"];
|
|
30
|
+
|
|
28
31
|
export const dropbox = (options: DropboxOptions) => {
|
|
29
32
|
const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token";
|
|
30
33
|
|
|
31
34
|
return {
|
|
32
35
|
id: "dropbox",
|
|
33
36
|
name: "Dropbox",
|
|
37
|
+
callbackPath: "/callback/dropbox",
|
|
34
38
|
createAuthorizationURL: async ({
|
|
35
39
|
state,
|
|
36
40
|
scopes,
|
|
37
41
|
codeVerifier,
|
|
38
42
|
redirectURI,
|
|
43
|
+
additionalParams,
|
|
39
44
|
}) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return await createAuthorizationURL({
|
|
45
|
+
const requestedScopes = resolveRequestedScopes(
|
|
46
|
+
options,
|
|
47
|
+
DROPBOX_DEFAULT_SCOPES,
|
|
48
|
+
scopes,
|
|
49
|
+
);
|
|
50
|
+
return createAuthorizationURL({
|
|
48
51
|
id: "dropbox",
|
|
49
52
|
options,
|
|
50
53
|
authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize",
|
|
51
|
-
scopes:
|
|
54
|
+
scopes: requestedScopes,
|
|
52
55
|
state,
|
|
53
56
|
redirectURI,
|
|
54
57
|
codeVerifier,
|
|
55
|
-
additionalParams
|
|
58
|
+
additionalParams: {
|
|
59
|
+
...(options.accessType
|
|
60
|
+
? { token_access_type: options.accessType }
|
|
61
|
+
: {}),
|
|
62
|
+
...(additionalParams ?? {}),
|
|
63
|
+
},
|
|
56
64
|
});
|
|
57
65
|
},
|
|
58
66
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -108,5 +116,5 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
108
116
|
};
|
|
109
117
|
},
|
|
110
118
|
options,
|
|
111
|
-
} satisfies
|
|
119
|
+
} satisfies UpstreamProvider<DropboxProfile>;
|
|
112
120
|
};
|