@better-auth/core 1.7.0-beta.7 → 1.7.0-beta.9
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 +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/error/codes.d.mts +0 -5
- package/dist/error/codes.mjs +0 -5
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/create-authorization-url.d.mts +1 -4
- package/dist/oauth2/create-authorization-url.mjs +1 -4
- package/dist/oauth2/index.d.mts +3 -4
- package/dist/oauth2/index.mjs +2 -3
- package/dist/oauth2/oauth-provider.d.mts +12 -50
- package/dist/oauth2/refresh-access-token.mjs +2 -1
- package/dist/oauth2/utils.d.mts +6 -1
- package/dist/oauth2/utils.mjs +24 -2
- package/dist/oauth2/verify-id-token.d.mts +6 -5
- package/dist/oauth2/verify-id-token.mjs +2 -2
- package/dist/social-providers/apple.d.mts +1 -5
- package/dist/social-providers/apple.mjs +5 -5
- package/dist/social-providers/atlassian.d.mts +1 -5
- package/dist/social-providers/atlassian.mjs +4 -4
- package/dist/social-providers/cognito.d.mts +1 -5
- package/dist/social-providers/cognito.mjs +11 -18
- package/dist/social-providers/discord.d.mts +1 -5
- package/dist/social-providers/discord.mjs +6 -7
- package/dist/social-providers/dropbox.d.mts +1 -5
- package/dist/social-providers/dropbox.mjs +5 -5
- package/dist/social-providers/facebook.d.mts +1 -5
- package/dist/social-providers/facebook.mjs +5 -5
- package/dist/social-providers/figma.d.mts +1 -5
- package/dist/social-providers/figma.mjs +5 -5
- package/dist/social-providers/github.d.mts +1 -5
- package/dist/social-providers/github.mjs +4 -4
- package/dist/social-providers/gitlab.d.mts +1 -5
- package/dist/social-providers/gitlab.mjs +6 -6
- package/dist/social-providers/google.d.mts +8 -10
- package/dist/social-providers/google.mjs +12 -13
- package/dist/social-providers/huggingface.d.mts +1 -5
- package/dist/social-providers/huggingface.mjs +8 -8
- package/dist/social-providers/index.d.mts +35 -177
- package/dist/social-providers/kakao.d.mts +1 -5
- package/dist/social-providers/kakao.mjs +8 -8
- package/dist/social-providers/kick.d.mts +1 -5
- package/dist/social-providers/kick.mjs +4 -4
- package/dist/social-providers/line.d.mts +1 -5
- package/dist/social-providers/line.mjs +10 -10
- package/dist/social-providers/linear.d.mts +1 -5
- package/dist/social-providers/linear.mjs +4 -4
- package/dist/social-providers/linkedin.d.mts +1 -5
- package/dist/social-providers/linkedin.mjs +10 -10
- package/dist/social-providers/microsoft-entra-id.d.mts +1 -5
- package/dist/social-providers/microsoft-entra-id.mjs +10 -11
- package/dist/social-providers/naver.d.mts +1 -5
- package/dist/social-providers/naver.mjs +4 -4
- package/dist/social-providers/notion.d.mts +1 -5
- package/dist/social-providers/notion.mjs +4 -4
- package/dist/social-providers/paybin.d.mts +1 -5
- package/dist/social-providers/paybin.mjs +10 -10
- package/dist/social-providers/paypal.d.mts +1 -5
- package/dist/social-providers/paypal.mjs +2 -8
- package/dist/social-providers/polar.d.mts +1 -5
- package/dist/social-providers/polar.mjs +8 -8
- package/dist/social-providers/railway.d.mts +1 -5
- package/dist/social-providers/railway.mjs +9 -9
- package/dist/social-providers/reddit.d.mts +1 -5
- package/dist/social-providers/reddit.mjs +5 -5
- package/dist/social-providers/roblox.d.mts +1 -5
- package/dist/social-providers/roblox.mjs +5 -5
- package/dist/social-providers/salesforce.d.mts +1 -5
- package/dist/social-providers/salesforce.mjs +8 -8
- package/dist/social-providers/slack.d.mts +1 -5
- package/dist/social-providers/slack.mjs +9 -9
- package/dist/social-providers/spotify.d.mts +1 -5
- package/dist/social-providers/spotify.mjs +5 -5
- package/dist/social-providers/tiktok.d.mts +1 -5
- package/dist/social-providers/tiktok.mjs +5 -9
- package/dist/social-providers/twitch.d.mts +1 -5
- package/dist/social-providers/twitch.mjs +4 -4
- package/dist/social-providers/twitter.d.mts +1 -5
- package/dist/social-providers/twitter.mjs +9 -9
- package/dist/social-providers/vercel.d.mts +1 -5
- package/dist/social-providers/vercel.mjs +7 -4
- package/dist/social-providers/vk.d.mts +1 -5
- package/dist/social-providers/vk.mjs +5 -5
- package/dist/social-providers/wechat.d.mts +1 -5
- package/dist/social-providers/wechat.mjs +5 -9
- package/dist/social-providers/zoom.d.mts +1 -6
- package/dist/social-providers/zoom.mjs +9 -15
- package/dist/types/context.d.mts +6 -2
- package/package.json +1 -1
- package/src/db/get-tables.ts +3 -8
- package/src/db/schema/account.ts +5 -14
- package/src/error/codes.ts +0 -5
- package/src/oauth2/create-authorization-url.ts +1 -1
- package/src/oauth2/index.ts +2 -12
- package/src/oauth2/oauth-provider.ts +11 -56
- package/src/oauth2/refresh-access-token.ts +3 -2
- package/src/oauth2/utils.ts +39 -1
- package/src/oauth2/verify-id-token.ts +7 -5
- package/src/social-providers/apple.ts +8 -13
- package/src/social-providers/atlassian.ts +8 -12
- package/src/social-providers/cognito.ts +11 -18
- package/src/social-providers/discord.ts +8 -19
- package/src/social-providers/dropbox.ts +7 -13
- package/src/social-providers/facebook.ts +9 -13
- package/src/social-providers/figma.ts +9 -13
- package/src/social-providers/github.ts +8 -12
- package/src/social-providers/gitlab.ts +8 -14
- package/src/social-providers/google.ts +23 -29
- package/src/social-providers/huggingface.ts +8 -12
- package/src/social-providers/kakao.ts +8 -16
- package/src/social-providers/kick.ts +7 -12
- package/src/social-providers/line.ts +10 -14
- package/src/social-providers/linear.ts +6 -12
- package/src/social-providers/linkedin.ts +10 -14
- package/src/social-providers/microsoft-entra-id.ts +8 -18
- package/src/social-providers/naver.ts +6 -12
- package/src/social-providers/notion.ts +6 -12
- package/src/social-providers/paybin.ts +11 -14
- package/src/social-providers/paypal.ts +8 -6
- package/src/social-providers/polar.ts +8 -12
- package/src/social-providers/railway.ts +9 -13
- package/src/social-providers/reddit.ts +7 -18
- package/src/social-providers/roblox.ts +7 -18
- package/src/social-providers/salesforce.ts +8 -12
- package/src/social-providers/slack.ts +9 -18
- package/src/social-providers/spotify.ts +7 -13
- package/src/social-providers/tiktok.ts +7 -13
- package/src/social-providers/twitch.ts +8 -12
- package/src/social-providers/twitter.ts +8 -17
- package/src/social-providers/vercel.ts +10 -16
- package/src/social-providers/vk.ts +7 -13
- package/src/social-providers/wechat.ts +8 -20
- package/src/social-providers/zoom.ts +6 -19
- package/src/types/context.ts +8 -2
- package/dist/oauth2/scopes.d.mts +0 -76
- package/dist/oauth2/scopes.mjs +0 -96
- package/src/oauth2/scopes.ts +0 -118
package/dist/types/context.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { BetterAuthOptions, BetterAuthRateLimitOptions, UserProvisioningSource }
|
|
|
10
10
|
import { Account } from "../db/schema/account.mjs";
|
|
11
11
|
import { BetterAuthCookie, BetterAuthCookies } from "./cookie.mjs";
|
|
12
12
|
import { SecretConfig } from "./secret.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { OAuthProvider } from "../oauth2/oauth-provider.mjs";
|
|
14
14
|
import { CookieOptions, EndpointContext } from "better-call";
|
|
15
15
|
|
|
16
16
|
//#region src/types/context.d.ts
|
|
@@ -54,6 +54,10 @@ type GenericEndpointContext<Options extends BetterAuthOptions = BetterAuthOption
|
|
|
54
54
|
context: AuthContext<Options>;
|
|
55
55
|
};
|
|
56
56
|
interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions> {
|
|
57
|
+
createOAuthUser(user: Omit<User, "id" | "createdAt" | "updatedAt">, account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> & Partial<Account>): Promise<{
|
|
58
|
+
user: User;
|
|
59
|
+
account: Account;
|
|
60
|
+
}>;
|
|
57
61
|
createUser<T extends Record<string, any>>(user: Omit<User, "id" | "createdAt" | "updatedAt" | "emailVerified"> & Partial<User> & Record<string, any>,
|
|
58
62
|
/**
|
|
59
63
|
* Provisioning source. The creation seam adds `action: "create-user"` and
|
|
@@ -232,7 +236,7 @@ type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> = Plugin
|
|
|
232
236
|
session: Session & Record<string, any>;
|
|
233
237
|
user: User & Record<string, any>;
|
|
234
238
|
} | null) => void;
|
|
235
|
-
socialProviders:
|
|
239
|
+
socialProviders: OAuthProvider[];
|
|
236
240
|
authCookies: BetterAuthCookies;
|
|
237
241
|
logger: ReturnType<typeof createLogger>;
|
|
238
242
|
rateLimit: {
|
package/package.json
CHANGED
package/src/db/get-tables.ts
CHANGED
|
@@ -261,15 +261,10 @@ export const getAuthTables = (
|
|
|
261
261
|
options.account?.fields?.refreshTokenExpiresAt ||
|
|
262
262
|
"refreshTokenExpiresAt",
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// value. Upgrading installs need a manual data migration (split
|
|
267
|
-
// legacy `scope` on comma/space, trim, drop empties, dedupe). Order
|
|
268
|
-
// is insignificant per RFC 6749 §3.3.
|
|
269
|
-
grantedScopes: {
|
|
270
|
-
type: "string[]",
|
|
264
|
+
scope: {
|
|
265
|
+
type: "string",
|
|
271
266
|
required: false,
|
|
272
|
-
fieldName: options.account?.fields?.
|
|
267
|
+
fieldName: options.account?.fields?.scope || "scope",
|
|
273
268
|
},
|
|
274
269
|
password: {
|
|
275
270
|
type: "string",
|
package/src/db/schema/account.ts
CHANGED
|
@@ -23,21 +23,12 @@ export const accountSchema = coreSchema.extend({
|
|
|
23
23
|
*/
|
|
24
24
|
refreshTokenExpiresAt: z.date().nullish(),
|
|
25
25
|
/**
|
|
26
|
-
* The scopes the user has granted
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Renamed from the legacy comma-joined `scope` string. Breaking, with no
|
|
32
|
-
* automatic data migration (and no read-time shim): the migration generator
|
|
33
|
-
* only adds the new `grantedScopes` column, so legacy accounts read as empty
|
|
34
|
-
* here until an upgrade backfills `grantedScopes` from the old `scope` values
|
|
35
|
-
* (split on comma/space, trim, drop empties, dedupe). See the release
|
|
36
|
-
* changeset for the backfill.
|
|
37
|
-
*
|
|
38
|
-
* @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
|
|
26
|
+
* The set of OAuth scopes the user has granted to this account, stored
|
|
27
|
+
* as a comma-separated list. Represents the accumulated grant rather
|
|
28
|
+
* than the latest token's `scope` claim, since per RFC 6749 Section 1.5 a
|
|
29
|
+
* token's scope may be narrower than the user's grant.
|
|
39
30
|
*/
|
|
40
|
-
|
|
31
|
+
scope: z.string().nullish(),
|
|
41
32
|
/**
|
|
42
33
|
* Password is only stored in the credential provider
|
|
43
34
|
*/
|
package/src/error/codes.ts
CHANGED
|
@@ -29,11 +29,6 @@ export const BASE_ERROR_CODES = defineErrorCodes({
|
|
|
29
29
|
TOKEN_EXPIRED: "Token expired",
|
|
30
30
|
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
|
|
31
31
|
FAILED_TO_GET_USER_INFO: "Failed to get user info",
|
|
32
|
-
PROVIDER_NOT_SUPPORTED: "Provider not supported",
|
|
33
|
-
TOKEN_REFRESH_NOT_SUPPORTED: "Token refresh not supported",
|
|
34
|
-
REFRESH_TOKEN_NOT_FOUND: "Refresh token not found",
|
|
35
|
-
FAILED_TO_GET_ACCESS_TOKEN: "Failed to get a valid access token",
|
|
36
|
-
FAILED_TO_REFRESH_ACCESS_TOKEN: "Failed to refresh access token",
|
|
37
32
|
USER_EMAIL_NOT_FOUND: "User email not found",
|
|
38
33
|
EMAIL_NOT_VERIFIED: "Email not verified",
|
|
39
34
|
PASSWORD_TOO_SHORT: "Password too short",
|
package/src/oauth2/index.ts
CHANGED
|
@@ -63,28 +63,17 @@ export {
|
|
|
63
63
|
verifyDpopProof,
|
|
64
64
|
} from "./dpop";
|
|
65
65
|
export type {
|
|
66
|
-
AuthorizationURLResult,
|
|
67
|
-
GrantAuthority,
|
|
68
66
|
OAuth2Tokens,
|
|
69
67
|
OAuth2UserInfo,
|
|
70
68
|
OAuthIdTokenConfig,
|
|
69
|
+
OAuthProvider,
|
|
71
70
|
OAuthRefreshContext,
|
|
72
|
-
ProviderGrantAuthority,
|
|
73
71
|
ProviderOptions,
|
|
74
|
-
UpstreamProvider,
|
|
75
72
|
} from "./oauth-provider";
|
|
76
73
|
export {
|
|
77
74
|
refreshAccessToken,
|
|
78
75
|
refreshAccessTokenRequest,
|
|
79
76
|
} from "./refresh-access-token";
|
|
80
|
-
export {
|
|
81
|
-
includesGrantedScope,
|
|
82
|
-
normalizeScopes,
|
|
83
|
-
parseScopeField,
|
|
84
|
-
readGrantedScopes,
|
|
85
|
-
resolveRequestedScopes,
|
|
86
|
-
unionGrantedScopes,
|
|
87
|
-
} from "./scopes";
|
|
88
77
|
export type {
|
|
89
78
|
TokenEndpointAuth,
|
|
90
79
|
TokenEndpointAuthMethod,
|
|
@@ -95,6 +84,7 @@ export {
|
|
|
95
84
|
generateCodeChallenge,
|
|
96
85
|
getOAuth2Tokens,
|
|
97
86
|
getPrimaryClientId,
|
|
87
|
+
mergeScopes,
|
|
98
88
|
} from "./utils";
|
|
99
89
|
export {
|
|
100
90
|
authorizationCodeRequest,
|
|
@@ -89,64 +89,22 @@ export interface OAuthRefreshContext {
|
|
|
89
89
|
request?: Request | undefined;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
* The result of building a provider authorization URL.
|
|
94
|
-
*
|
|
95
|
-
* `requestedScopes` is the effective set of scopes encoded in the URL (the
|
|
96
|
-
* provider's built-in defaults + configured `options.scope` + per-request
|
|
97
|
-
* `scopes`, composed by `resolveRequestedScopes`). Callers persist it so the
|
|
98
|
-
* callback can fall back to the request when the provider omits `scope` from
|
|
99
|
-
* its token response (RFC 6749 §5.1).
|
|
100
|
-
*/
|
|
101
|
-
export interface AuthorizationURLResult {
|
|
102
|
-
url: URL;
|
|
103
|
-
requestedScopes: string[];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* How much an RP trusts a provider's echoed token-response `scope` when
|
|
108
|
-
* persisting `account.grantedScopes`.
|
|
109
|
-
*
|
|
110
|
-
* - `"full-grant"`: the echo is the user's complete current grant, so the seam
|
|
111
|
-
* replaces the stored grant with it. This is the only path that may narrow
|
|
112
|
-
* the grant. Declare it only for providers whose token response reports the
|
|
113
|
-
* full combined grant, e.g. Google with `include_granted_scopes`.
|
|
114
|
-
* - `"projection"`: the echo is this request's subset, so the seam unions it
|
|
115
|
-
* onto the stored grant. The safe default for every provider.
|
|
116
|
-
* - `"absent-echo"`: the provider omitted `scope`, so the grant equals what was
|
|
117
|
-
* requested (RFC 6749 §5.1) and the seam unions the requested set. Resolved
|
|
118
|
-
* at runtime by the persistence seam, never declared by a provider.
|
|
119
|
-
*
|
|
120
|
-
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
|
|
121
|
-
*/
|
|
122
|
-
export type GrantAuthority = "full-grant" | "projection" | "absent-echo";
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The authority a provider may declare for its own echoed scope. `"absent-echo"`
|
|
126
|
-
* is excluded because it is a runtime condition (an omitted echo), not a
|
|
127
|
-
* provider trait.
|
|
128
|
-
*/
|
|
129
|
-
export type ProviderGrantAuthority = Exclude<GrantAuthority, "absent-echo">;
|
|
130
|
-
|
|
131
|
-
export interface UpstreamProvider<
|
|
92
|
+
export interface OAuthProvider<
|
|
132
93
|
T extends Record<string, any> = Record<string, any>,
|
|
133
94
|
O extends Record<string, any> = Partial<ProviderOptions>,
|
|
134
95
|
> {
|
|
135
96
|
id: LiteralString;
|
|
136
97
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* How the persistence seam treats this provider's echoed token-response
|
|
143
|
-
* `scope`. Declare `"full-grant"` only when the echo is the user's complete
|
|
144
|
-
* current grant (e.g. Google with `include_granted_scopes`); otherwise the
|
|
145
|
-
* echo is unioned onto the stored grant.
|
|
98
|
+
* Optional path under the resolved per-request `baseURL` where this
|
|
99
|
+
* provider's OAuth callback handler is mounted. Providers that use the
|
|
100
|
+
* shared `/callback/<id>` route can omit this.
|
|
101
|
+
*
|
|
102
|
+
* Custom paths must start with `/`.
|
|
146
103
|
*
|
|
147
|
-
*
|
|
104
|
+
* Endpoints compose `redirectURI = ctx.context.baseURL + callbackPath` per
|
|
105
|
+
* request, so the provider must not hardcode an origin or `baseURL` here.
|
|
148
106
|
*/
|
|
149
|
-
|
|
107
|
+
callbackPath?: string | undefined;
|
|
150
108
|
createAuthorizationURL: (data: {
|
|
151
109
|
state: string;
|
|
152
110
|
codeVerifier: string;
|
|
@@ -167,7 +125,7 @@ export interface UpstreamProvider<
|
|
|
167
125
|
* before applying them.
|
|
168
126
|
*/
|
|
169
127
|
additionalParams?: Record<string, string> | undefined;
|
|
170
|
-
}) => Awaitable<
|
|
128
|
+
}) => Awaitable<URL>;
|
|
171
129
|
name: string;
|
|
172
130
|
validateAuthorizationCode: (data: {
|
|
173
131
|
code: string;
|
|
@@ -214,6 +172,7 @@ export interface UpstreamProvider<
|
|
|
214
172
|
ctx?: OAuthRefreshContext,
|
|
215
173
|
) => Promise<OAuth2Tokens>)
|
|
216
174
|
| undefined;
|
|
175
|
+
revokeToken?: ((token: string) => Promise<void>) | undefined;
|
|
217
176
|
/**
|
|
218
177
|
* Declarative id_token verification config consumed by the shared
|
|
219
178
|
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
@@ -321,10 +280,6 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
321
280
|
emailVerified: boolean;
|
|
322
281
|
[key: string]: any;
|
|
323
282
|
};
|
|
324
|
-
// TODO: type as `Profile` once provider getUserInfo paths that return a
|
|
325
|
-
// narrower data shape than their declared profile are reconciled; today
|
|
326
|
-
// `any` is load-bearing for those (e.g. facebook) and tightening it ripples
|
|
327
|
-
// across ~10 providers, out of scope for the grant refactor.
|
|
328
283
|
data: any;
|
|
329
284
|
} | null>)
|
|
330
285
|
| undefined;
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TokenEndpointSecretAuthentication,
|
|
7
7
|
} from "./token-endpoint-auth";
|
|
8
8
|
import { applyTokenEndpointAuth } from "./token-endpoint-auth";
|
|
9
|
+
import { parseScopeField } from "./utils";
|
|
9
10
|
|
|
10
11
|
interface RefreshAccessTokenRequestInput {
|
|
11
12
|
refreshToken: string;
|
|
@@ -143,7 +144,7 @@ export async function refreshAccessToken({
|
|
|
143
144
|
expires_in?: number | undefined;
|
|
144
145
|
refresh_token_expires_in?: number | undefined;
|
|
145
146
|
token_type?: string | undefined;
|
|
146
|
-
scope?:
|
|
147
|
+
scope?: unknown;
|
|
147
148
|
id_token?: string | undefined;
|
|
148
149
|
}>(tokenEndpoint, {
|
|
149
150
|
method: "POST",
|
|
@@ -157,7 +158,7 @@ export async function refreshAccessToken({
|
|
|
157
158
|
accessToken: data.access_token,
|
|
158
159
|
refreshToken: data.refresh_token,
|
|
159
160
|
tokenType: data.token_type,
|
|
160
|
-
scopes:
|
|
161
|
+
scopes: parseScopeField(data.scope),
|
|
161
162
|
idToken: data.id_token,
|
|
162
163
|
};
|
|
163
164
|
|
package/src/oauth2/utils.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { base64Url } from "@better-auth/utils/base64";
|
|
2
2
|
import type { OAuth2Tokens } from "./oauth-provider";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a provider's `scope` token-response field into a string array.
|
|
6
|
+
*
|
|
7
|
+
* RFC 6749 Section 3.3 defines `scope` as a space-delimited string, but
|
|
8
|
+
* providers vary: some return an already-split array. Accept both forms and
|
|
9
|
+
* drop empty or non-string entries.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/better-auth/better-auth/issues/9076
|
|
12
|
+
*/
|
|
13
|
+
export function parseScopeField(scope: unknown): string[] {
|
|
14
|
+
if (Array.isArray(scope)) {
|
|
15
|
+
return scope
|
|
16
|
+
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
if (typeof scope === "string") {
|
|
20
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
6
26
|
const getDate = (seconds: number) => {
|
|
@@ -44,6 +64,24 @@ export function applyDefaultAccessTokenExpiry(
|
|
|
44
64
|
return tokens;
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Compute the union of stored and incoming OAuth scopes, preserving
|
|
69
|
+
* stored insertion order and dropping duplicates.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeScopes(
|
|
72
|
+
stored: string | null | undefined,
|
|
73
|
+
incoming: string[] | undefined,
|
|
74
|
+
): string {
|
|
75
|
+
const existing = stored
|
|
76
|
+
? stored
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((scope) => scope.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
: [];
|
|
81
|
+
const next = (incoming ?? []).map((scope) => scope.trim()).filter(Boolean);
|
|
82
|
+
return [...new Set([...existing, ...next])].join(",");
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
/**
|
|
48
86
|
* Return the provider's primary Client ID: the single string, or the entry at
|
|
49
87
|
* array index 0 for the cross-platform form used by ID token audience
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { decodeProtectedHeader, jwtVerify } from "jose";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OAuthProvider, ProviderOptions } from "./oauth-provider";
|
|
3
|
+
|
|
4
|
+
type ProviderWithIdTokenConfig = Pick<OAuthProvider, "idToken" | "options">;
|
|
3
5
|
|
|
4
6
|
async function sha256Hex(value: string) {
|
|
5
7
|
const data = new TextEncoder().encode(value);
|
|
@@ -29,12 +31,12 @@ async function nonceMatches(
|
|
|
29
31
|
/**
|
|
30
32
|
* Whether a provider can verify a client-submitted id_token.
|
|
31
33
|
*
|
|
32
|
-
* A provider supports id_token sign-in when it declares an {@link
|
|
34
|
+
* A provider supports id_token sign-in when it declares an {@link OAuthProvider.idToken}
|
|
33
35
|
* verification config, or when the integrator supplies a `verifyIdToken` override on the
|
|
34
36
|
* provider options. A provider whose options set `disableIdTokenSignIn`, or that declares
|
|
35
37
|
* neither, rejects the client id_token sign-in path with `ID_TOKEN_NOT_SUPPORTED`.
|
|
36
38
|
*/
|
|
37
|
-
export function supportsIdTokenSignIn(provider:
|
|
39
|
+
export function supportsIdTokenSignIn(provider: ProviderWithIdTokenConfig) {
|
|
38
40
|
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
39
41
|
if (options.disableIdTokenSignIn) {
|
|
40
42
|
return false;
|
|
@@ -46,7 +48,7 @@ export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
|
|
|
46
48
|
* Verify a client-submitted id_token against a provider's verification config.
|
|
47
49
|
*
|
|
48
50
|
* This is the single id_token verifier for every social provider. Providers no longer
|
|
49
|
-
* implement their own boolean `verifyIdToken`; they declare an {@link
|
|
51
|
+
* implement their own boolean `verifyIdToken`; they declare an {@link OAuthProvider.idToken}
|
|
50
52
|
* config and this function performs the cryptographic check. The contract is fail-closed: a
|
|
51
53
|
* provider without a config (and without an integrator `verifyIdToken` override) returns
|
|
52
54
|
* `false`, so a forged token can never be accepted by omission.
|
|
@@ -54,7 +56,7 @@ export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
|
|
|
54
56
|
* @returns `true` only when the token is authentic for the provider.
|
|
55
57
|
*/
|
|
56
58
|
export async function verifyProviderIdToken(
|
|
57
|
-
provider:
|
|
59
|
+
provider: ProviderWithIdTokenConfig,
|
|
58
60
|
token: string,
|
|
59
61
|
nonce?: string,
|
|
60
62
|
): Promise<boolean> {
|
|
@@ -3,12 +3,11 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
3
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 { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
7
7
|
import {
|
|
8
8
|
createAuthorizationURL,
|
|
9
9
|
getPrimaryClientId,
|
|
10
10
|
refreshAccessToken,
|
|
11
|
-
resolveRequestedScopes,
|
|
12
11
|
validateAuthorizationCode,
|
|
13
12
|
} from "../oauth2";
|
|
14
13
|
export interface AppleProfile {
|
|
@@ -78,14 +77,11 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
|
|
|
78
77
|
audience?: (string | string[]) | undefined;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
const APPLE_DEFAULT_SCOPES = ["email", "name"];
|
|
82
|
-
|
|
83
80
|
export const apple = (options: AppleOptions) => {
|
|
84
81
|
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
85
82
|
return {
|
|
86
83
|
id: "apple",
|
|
87
84
|
name: "Apple",
|
|
88
|
-
callbackPath: "/callback/apple",
|
|
89
85
|
async createAuthorizationURL({
|
|
90
86
|
state,
|
|
91
87
|
scopes,
|
|
@@ -98,22 +94,21 @@ export const apple = (options: AppleOptions) => {
|
|
|
98
94
|
);
|
|
99
95
|
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
100
96
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
return createAuthorizationURL({
|
|
97
|
+
const _scope = options.disableDefaultScope ? [] : ["email", "name"];
|
|
98
|
+
if (options.scope) _scope.push(...options.scope);
|
|
99
|
+
if (scopes) _scope.push(...scopes);
|
|
100
|
+
const url = await createAuthorizationURL({
|
|
107
101
|
id: "apple",
|
|
108
102
|
options,
|
|
109
103
|
authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
|
|
110
|
-
scopes:
|
|
104
|
+
scopes: _scope,
|
|
111
105
|
state,
|
|
112
106
|
redirectURI,
|
|
113
107
|
responseMode: "form_post",
|
|
114
108
|
responseType: "code id_token",
|
|
115
109
|
additionalParams,
|
|
116
110
|
});
|
|
111
|
+
return url;
|
|
117
112
|
},
|
|
118
113
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
119
114
|
return validateAuthorizationCode({
|
|
@@ -189,7 +184,7 @@ export const apple = (options: AppleOptions) => {
|
|
|
189
184
|
};
|
|
190
185
|
},
|
|
191
186
|
options,
|
|
192
|
-
} satisfies
|
|
187
|
+
} satisfies OAuthProvider<AppleProfile>;
|
|
193
188
|
};
|
|
194
189
|
|
|
195
190
|
export const getApplePublicKey = async (kid: string) => {
|
|
@@ -1,11 +1,10 @@
|
|
|
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 { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
5
5
|
import {
|
|
6
6
|
createAuthorizationURL,
|
|
7
7
|
refreshAccessToken,
|
|
8
|
-
resolveRequestedScopes,
|
|
9
8
|
validateAuthorizationCode,
|
|
10
9
|
} from "../oauth2";
|
|
11
10
|
|
|
@@ -30,14 +29,11 @@ export interface AtlassianOptions extends ProviderOptions<AtlassianProfile> {
|
|
|
30
29
|
clientId: string;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const ATLASSIAN_DEFAULT_SCOPES = ["read:jira-user", "offline_access"];
|
|
34
|
-
|
|
35
32
|
export const atlassian = (options: AtlassianOptions) => {
|
|
36
33
|
const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
|
|
37
34
|
return {
|
|
38
35
|
id: "atlassian",
|
|
39
36
|
name: "Atlassian",
|
|
40
|
-
callbackPath: "/callback/atlassian",
|
|
41
37
|
|
|
42
38
|
async createAuthorizationURL({
|
|
43
39
|
state,
|
|
@@ -54,17 +50,17 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
54
50
|
throw new BetterAuthError("codeVerifier is required for Atlassian");
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
53
|
+
const _scopes = options.disableDefaultScope
|
|
54
|
+
? []
|
|
55
|
+
: ["read:jira-user", "offline_access"];
|
|
56
|
+
if (options.scope) _scopes.push(...options.scope);
|
|
57
|
+
if (scopes) _scopes.push(...scopes);
|
|
62
58
|
|
|
63
59
|
return createAuthorizationURL({
|
|
64
60
|
id: "atlassian",
|
|
65
61
|
options,
|
|
66
62
|
authorizationEndpoint: "https://auth.atlassian.com/authorize",
|
|
67
|
-
scopes:
|
|
63
|
+
scopes: _scopes,
|
|
68
64
|
state,
|
|
69
65
|
codeVerifier,
|
|
70
66
|
redirectURI,
|
|
@@ -140,5 +136,5 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
140
136
|
},
|
|
141
137
|
|
|
142
138
|
options,
|
|
143
|
-
} satisfies
|
|
139
|
+
} satisfies OAuthProvider<AtlassianProfile>;
|
|
144
140
|
};
|
|
@@ -2,12 +2,11 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
2
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 { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
8
|
getPrimaryClientId,
|
|
9
9
|
refreshAccessToken,
|
|
10
|
-
resolveRequestedScopes,
|
|
11
10
|
validateAuthorizationCode,
|
|
12
11
|
} from "../oauth2";
|
|
13
12
|
|
|
@@ -58,8 +57,6 @@ export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
|
58
57
|
identityProvider?: string | undefined;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
const COGNITO_DEFAULT_SCOPES = ["openid", "profile", "email"];
|
|
62
|
-
|
|
63
60
|
export const cognito = (options: CognitoOptions) => {
|
|
64
61
|
if (!options.domain || !options.region || !options.userPoolId) {
|
|
65
62
|
logger.error(
|
|
@@ -76,7 +73,6 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
76
73
|
return {
|
|
77
74
|
id: "cognito",
|
|
78
75
|
name: "Cognito",
|
|
79
|
-
callbackPath: "/callback/cognito",
|
|
80
76
|
async createAuthorizationURL({
|
|
81
77
|
state,
|
|
82
78
|
scopes,
|
|
@@ -97,19 +93,19 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
97
93
|
);
|
|
98
94
|
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
|
|
99
95
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
96
|
+
const _scopes = options.disableDefaultScope
|
|
97
|
+
? []
|
|
98
|
+
: ["openid", "profile", "email"];
|
|
99
|
+
if (options.scope) _scopes.push(...options.scope);
|
|
100
|
+
if (scopes) _scopes.push(...scopes);
|
|
105
101
|
|
|
106
|
-
const
|
|
102
|
+
const url = await createAuthorizationURL({
|
|
107
103
|
id: "cognito",
|
|
108
104
|
options: {
|
|
109
105
|
...options,
|
|
110
106
|
},
|
|
111
107
|
authorizationEndpoint,
|
|
112
|
-
scopes:
|
|
108
|
+
scopes: _scopes,
|
|
113
109
|
state,
|
|
114
110
|
codeVerifier,
|
|
115
111
|
redirectURI,
|
|
@@ -130,12 +126,9 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
130
126
|
// Manually append the scope with proper encoding to the URL
|
|
131
127
|
const urlString = url.toString();
|
|
132
128
|
const separator = urlString.includes("?") ? "&" : "?";
|
|
133
|
-
return {
|
|
134
|
-
url: new URL(`${urlString}${separator}scope=${encodedScope}`),
|
|
135
|
-
requestedScopes,
|
|
136
|
-
};
|
|
129
|
+
return new URL(`${urlString}${separator}scope=${encodedScope}`);
|
|
137
130
|
}
|
|
138
|
-
return
|
|
131
|
+
return url;
|
|
139
132
|
},
|
|
140
133
|
|
|
141
134
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
@@ -243,7 +236,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
243
236
|
},
|
|
244
237
|
|
|
245
238
|
options,
|
|
246
|
-
} satisfies
|
|
239
|
+
} satisfies OAuthProvider<CognitoProfile>;
|
|
247
240
|
};
|
|
248
241
|
|
|
249
242
|
export const getCognitoPublicKey = async (
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
3
3
|
import {
|
|
4
4
|
createAuthorizationURL,
|
|
5
5
|
refreshAccessToken,
|
|
6
|
-
resolveRequestedScopes,
|
|
7
6
|
validateAuthorizationCode,
|
|
8
7
|
} from "../oauth2";
|
|
9
8
|
export interface DiscordProfile extends Record<string, any> {
|
|
@@ -84,31 +83,21 @@ export interface DiscordOptions extends ProviderOptions<DiscordProfile> {
|
|
|
84
83
|
permissions?: number | undefined;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
const DISCORD_DEFAULT_SCOPES = ["identify", "email"];
|
|
88
|
-
|
|
89
86
|
export const discord = (options: DiscordOptions) => {
|
|
90
87
|
const tokenEndpoint = "https://discord.com/api/oauth2/token";
|
|
91
88
|
return {
|
|
92
89
|
id: "discord",
|
|
93
90
|
name: "Discord",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
additionalParams,
|
|
100
|
-
}) {
|
|
101
|
-
const requestedScopes = resolveRequestedScopes(
|
|
102
|
-
options,
|
|
103
|
-
DISCORD_DEFAULT_SCOPES,
|
|
104
|
-
scopes,
|
|
105
|
-
);
|
|
106
|
-
const hasBotScope = requestedScopes.includes("bot");
|
|
91
|
+
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
92
|
+
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
|
|
93
|
+
if (scopes) _scopes.push(...scopes);
|
|
94
|
+
if (options.scope) _scopes.push(...options.scope);
|
|
95
|
+
const hasBotScope = _scopes.includes("bot");
|
|
107
96
|
return createAuthorizationURL({
|
|
108
97
|
id: "discord",
|
|
109
98
|
options,
|
|
110
99
|
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
|
|
111
|
-
scopes:
|
|
100
|
+
scopes: _scopes,
|
|
112
101
|
state,
|
|
113
102
|
redirectURI,
|
|
114
103
|
prompt: options.prompt || "none",
|
|
@@ -181,5 +170,5 @@ export const discord = (options: DiscordOptions) => {
|
|
|
181
170
|
};
|
|
182
171
|
},
|
|
183
172
|
options,
|
|
184
|
-
} satisfies
|
|
173
|
+
} satisfies OAuthProvider<DiscordProfile>;
|
|
185
174
|
};
|