@better-auth/core 1.7.0-rc.1 → 1.7.0-rc.3
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 +28 -28
- package/dist/async_hooks/index.d.mts +0 -1
- package/dist/async_hooks/pure.index.d.mts +0 -1
- package/dist/context/endpoint-context.d.mts +1 -1
- package/dist/context/global.d.mts +0 -1
- package/dist/context/global.mjs +1 -1
- package/dist/context/request-state.d.mts +0 -1
- package/dist/context/request-state.mjs +8 -5
- package/dist/context/transaction.d.mts +8 -3
- package/dist/context/transaction.mjs +27 -10
- package/dist/db/adapter/factory.d.mts +3 -5
- package/dist/db/adapter/factory.mjs +7 -5
- package/dist/db/adapter/get-default-field-name.d.mts +2 -9
- package/dist/db/adapter/get-default-model-name.d.mts +1 -5
- package/dist/db/adapter/get-default-model-name.mjs +6 -5
- package/dist/db/adapter/get-field-attributes.d.mts +4 -11
- package/dist/db/adapter/get-field-name.d.mts +2 -9
- package/dist/db/adapter/get-id-field.d.mts +4 -12
- package/dist/db/adapter/get-model-name.d.mts +1 -5
- package/dist/db/adapter/index.d.mts +7 -32
- package/dist/db/adapter/types.d.mts +5 -17
- package/dist/db/adapter/utils.d.mts +0 -1
- package/dist/db/database-index.d.mts +44 -0
- package/dist/db/database-index.mjs +168 -0
- package/dist/db/get-tables.d.mts +7 -1
- package/dist/db/get-tables.mjs +47 -6
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/internal.d.mts +3 -0
- package/dist/db/internal.mjs +3 -0
- package/dist/db/plugin.d.mts +4 -3
- package/dist/db/schema/account.d.mts +16 -3
- package/dist/db/schema/account.mjs +35 -2
- package/dist/db/schema/rate-limit.d.mts +1 -1
- package/dist/db/schema/rate-limit.mjs +9 -0
- package/dist/db/schema/session.d.mts +1 -1
- package/dist/db/schema/shared.d.mts +0 -1
- package/dist/db/schema/user.d.mts +1 -1
- package/dist/db/schema/verification.d.mts +1 -1
- package/dist/db/type.d.mts +17 -9
- package/dist/env/logger.d.mts +25 -25
- package/dist/error/codes.d.mts +0 -1
- package/dist/error/index.d.mts +0 -1
- package/dist/index.d.mts +1 -1
- package/dist/instrumentation/attributes.d.mts +0 -1
- package/dist/instrumentation/pure.index.d.mts +0 -1
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +0 -1
- package/dist/oauth2/client-assertion.d.mts +3 -13
- package/dist/oauth2/client-credentials-token.d.mts +3 -17
- package/dist/oauth2/client-credentials-token.mjs +1 -4
- package/dist/oauth2/create-authorization-url.d.mts +3 -21
- package/dist/oauth2/dpop.d.mts +2 -23
- package/dist/oauth2/dpop.mjs +1 -1
- package/dist/oauth2/index.d.mts +3 -3
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/oauth-provider.d.mts +94 -36
- package/dist/oauth2/refresh-access-token.d.mts +3 -19
- package/dist/oauth2/refresh-access-token.mjs +3 -9
- package/dist/oauth2/reject-redirects.mjs +1 -1
- package/dist/oauth2/token-endpoint-auth.d.mts +0 -1
- package/dist/oauth2/utils.d.mts +0 -1
- package/dist/oauth2/utils.mjs +2 -3
- package/dist/oauth2/validate-authorization-code.d.mts +5 -29
- package/dist/oauth2/verify-id-token.d.mts +3 -2
- package/dist/oauth2/verify-id-token.mjs +3 -3
- package/dist/oauth2/verify.d.mts +18 -9
- package/dist/oauth2/verify.mjs +68 -5
- package/dist/social-providers/apple.d.mts +9 -24
- package/dist/social-providers/apple.mjs +2 -1
- package/dist/social-providers/atlassian.d.mts +18 -19
- package/dist/social-providers/atlassian.mjs +1 -1
- package/dist/social-providers/cognito.d.mts +33 -22
- package/dist/social-providers/cognito.mjs +2 -2
- package/dist/social-providers/discord.d.mts +7 -20
- package/dist/social-providers/discord.mjs +1 -1
- package/dist/social-providers/dropbox.d.mts +7 -22
- package/dist/social-providers/dropbox.mjs +1 -1
- package/dist/social-providers/facebook.d.mts +19 -27
- package/dist/social-providers/facebook.mjs +6 -17
- package/dist/social-providers/figma.d.mts +7 -22
- package/dist/social-providers/figma.mjs +1 -1
- package/dist/social-providers/github.d.mts +7 -23
- package/dist/social-providers/github.mjs +1 -1
- package/dist/social-providers/gitlab.d.mts +6 -43
- package/dist/social-providers/gitlab.mjs +1 -1
- package/dist/social-providers/google.d.mts +10 -32
- package/dist/social-providers/google.mjs +2 -1
- package/dist/social-providers/huggingface.d.mts +7 -22
- package/dist/social-providers/huggingface.mjs +1 -1
- package/dist/social-providers/index.d.mts +229 -787
- package/dist/social-providers/kakao.d.mts +8 -33
- package/dist/social-providers/kakao.mjs +1 -1
- package/dist/social-providers/kick.d.mts +7 -22
- package/dist/social-providers/kick.mjs +1 -1
- package/dist/social-providers/line.d.mts +8 -44
- package/dist/social-providers/line.mjs +4 -6
- package/dist/social-providers/linear.d.mts +7 -21
- package/dist/social-providers/linear.mjs +1 -1
- package/dist/social-providers/linkedin.d.mts +7 -21
- package/dist/social-providers/linkedin.mjs +1 -1
- package/dist/social-providers/microsoft-entra-id.d.mts +8 -18
- package/dist/social-providers/microsoft-entra-id.mjs +24 -4
- package/dist/social-providers/naver.d.mts +27 -42
- package/dist/social-providers/naver.mjs +1 -1
- package/dist/social-providers/notion.d.mts +7 -21
- package/dist/social-providers/notion.mjs +1 -1
- package/dist/social-providers/paybin.d.mts +8 -23
- package/dist/social-providers/paybin.mjs +2 -1
- package/dist/social-providers/paypal.d.mts +8 -33
- package/dist/social-providers/paypal.mjs +1 -1
- package/dist/social-providers/polar.d.mts +7 -22
- package/dist/social-providers/polar.mjs +1 -1
- package/dist/social-providers/railway.d.mts +7 -22
- package/dist/social-providers/railway.mjs +1 -1
- package/dist/social-providers/reddit.d.mts +7 -20
- package/dist/social-providers/reddit.mjs +1 -1
- package/dist/social-providers/roblox.d.mts +7 -20
- package/dist/social-providers/roblox.mjs +1 -1
- package/dist/social-providers/salesforce.d.mts +7 -22
- package/dist/social-providers/salesforce.mjs +1 -1
- package/dist/social-providers/slack.d.mts +7 -20
- package/dist/social-providers/slack.mjs +1 -1
- package/dist/social-providers/spotify.d.mts +7 -22
- package/dist/social-providers/spotify.mjs +1 -1
- package/dist/social-providers/tiktok.d.mts +10 -22
- package/dist/social-providers/tiktok.mjs +1 -1
- package/dist/social-providers/twitch.d.mts +7 -20
- package/dist/social-providers/twitch.mjs +1 -1
- package/dist/social-providers/twitter.d.mts +29 -26
- package/dist/social-providers/twitter.mjs +1 -1
- package/dist/social-providers/vercel.d.mts +7 -22
- package/dist/social-providers/vercel.mjs +1 -1
- package/dist/social-providers/vk.d.mts +8 -24
- package/dist/social-providers/vk.mjs +5 -6
- package/dist/social-providers/wechat.d.mts +7 -19
- package/dist/social-providers/wechat.mjs +1 -1
- package/dist/social-providers/zoom.d.mts +9 -25
- package/dist/social-providers/zoom.mjs +1 -1
- package/dist/types/context.d.mts +16 -9
- package/dist/types/cookie.d.mts +0 -1
- package/dist/types/helper.d.mts +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +37 -26
- package/dist/types/plugin-client.d.mts +1 -3
- package/dist/types/plugin.d.mts +2 -2
- package/dist/utils/async.d.mts +0 -1
- package/dist/utils/db.d.mts +1 -0
- package/dist/utils/deprecate.d.mts +0 -1
- package/dist/utils/error-codes.d.mts +2 -2
- package/dist/utils/host.d.mts +27 -1
- package/dist/utils/host.mjs +1 -1
- package/dist/utils/ip.d.mts +1 -0
- package/dist/utils/ip.mjs +1 -1
- package/dist/utils/is-api-error.d.mts +0 -1
- package/dist/utils/redirect-uri.d.mts +6 -2
- package/dist/utils/redirect-uri.mjs +11 -1
- package/package.json +10 -2
- package/src/context/request-state.ts +26 -6
- package/src/context/transaction.ts +37 -18
- package/src/db/adapter/factory.ts +34 -28
- package/src/db/adapter/get-default-model-name.ts +23 -14
- package/src/db/database-index.ts +351 -0
- package/src/db/get-tables.ts +89 -9
- package/src/db/index.ts +4 -0
- package/src/db/internal.ts +12 -0
- package/src/db/plugin.ts +3 -1
- package/src/db/schema/account.ts +27 -1
- package/src/db/schema/user.ts +1 -1
- package/src/db/type.ts +12 -0
- package/src/error/codes.ts +1 -0
- package/src/oauth2/index.ts +5 -0
- package/src/oauth2/oauth-provider.ts +105 -36
- package/src/oauth2/verify-id-token.ts +4 -2
- package/src/oauth2/verify.ts +121 -12
- package/src/social-providers/apple.ts +2 -1
- package/src/social-providers/atlassian.ts +1 -1
- package/src/social-providers/cognito.ts +2 -2
- package/src/social-providers/discord.ts +1 -1
- package/src/social-providers/dropbox.ts +1 -1
- package/src/social-providers/facebook.ts +21 -28
- package/src/social-providers/figma.ts +1 -1
- package/src/social-providers/github.ts +1 -1
- package/src/social-providers/gitlab.ts +1 -1
- package/src/social-providers/google.ts +2 -1
- package/src/social-providers/huggingface.ts +1 -1
- package/src/social-providers/kakao.ts +1 -1
- package/src/social-providers/kick.ts +1 -1
- package/src/social-providers/line.ts +7 -12
- package/src/social-providers/linear.ts +1 -1
- package/src/social-providers/linkedin.ts +1 -1
- package/src/social-providers/microsoft-entra-id.ts +36 -27
- package/src/social-providers/naver.ts +1 -1
- package/src/social-providers/notion.ts +1 -1
- package/src/social-providers/paybin.ts +2 -1
- package/src/social-providers/paypal.ts +1 -1
- package/src/social-providers/polar.ts +1 -1
- package/src/social-providers/railway.ts +1 -1
- package/src/social-providers/reddit.ts +1 -1
- package/src/social-providers/roblox.ts +1 -1
- package/src/social-providers/salesforce.ts +1 -1
- package/src/social-providers/slack.ts +1 -1
- package/src/social-providers/spotify.ts +1 -1
- package/src/social-providers/tiktok.ts +2 -2
- package/src/social-providers/twitch.ts +1 -1
- package/src/social-providers/twitter.ts +1 -1
- package/src/social-providers/vercel.ts +1 -1
- package/src/social-providers/vk.ts +2 -2
- package/src/social-providers/wechat.ts +1 -1
- package/src/social-providers/zoom.ts +1 -1
- package/src/types/context.ts +21 -14
- package/src/types/init-options.ts +15 -14
- package/src/utils/redirect-uri.ts +20 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type BoundedDatabaseIndexDialect,
|
|
3
|
+
type DBTableIndexSource,
|
|
4
|
+
getDatabaseFieldIndexName,
|
|
5
|
+
getDatabaseIndexName,
|
|
6
|
+
getDatabaseIndexStringLength,
|
|
7
|
+
getPortableDatabaseIdentifierKey,
|
|
8
|
+
type ResolvedDBTableIndex,
|
|
9
|
+
resolveDatabaseSchemaIndexes,
|
|
10
|
+
resolveDatabaseTableIndexes,
|
|
11
|
+
} from "./database-index";
|
|
12
|
+
export { getAuthTablesWithResolvedIndexes } from "./get-tables";
|
package/src/db/plugin.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { DBFieldAttribute } from "./type";
|
|
1
|
+
import type { DBFieldAttribute, DBTableIndex } from "./type";
|
|
2
2
|
|
|
3
3
|
export type BetterAuthPluginDBSchema = {
|
|
4
4
|
[table in string]: {
|
|
5
5
|
fields: {
|
|
6
6
|
[field: string]: DBFieldAttribute;
|
|
7
7
|
};
|
|
8
|
+
/** Table-level indexes, including compound indexes. */
|
|
9
|
+
indexes?: readonly DBTableIndex[] | undefined;
|
|
8
10
|
disableMigration?: boolean | undefined;
|
|
9
11
|
modelName?: string | undefined;
|
|
10
12
|
};
|
package/src/db/schema/account.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { coreSchema } from "./shared";
|
|
|
9
9
|
|
|
10
10
|
export const accountSchema = coreSchema.extend({
|
|
11
11
|
providerId: z.string(),
|
|
12
|
-
|
|
12
|
+
issuer: z.string(),
|
|
13
|
+
providerAccountId: z.string(),
|
|
13
14
|
userId: z.coerce.string(),
|
|
14
15
|
accessToken: z.string().nullish(),
|
|
15
16
|
refreshToken: z.string().nullish(),
|
|
@@ -37,6 +38,31 @@ export const accountSchema = coreSchema.extend({
|
|
|
37
38
|
|
|
38
39
|
export type BaseAccount = z.infer<typeof accountSchema>;
|
|
39
40
|
|
|
41
|
+
/** The stable provider-side key used to recognize an account. */
|
|
42
|
+
export type AccountKey = Readonly<
|
|
43
|
+
Pick<BaseAccount, "issuer" | "providerAccountId">
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
function encodeAccountIssuerProviderId(providerId: string): string {
|
|
47
|
+
return encodeURIComponent(providerId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates the synthetic issuer used by providers without an issuer of their own.
|
|
52
|
+
*/
|
|
53
|
+
export function createLocalAccountIssuer(providerId: string): string {
|
|
54
|
+
return `local:${encodeAccountIssuerProviderId(providerId)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates the synthetic issuer used by OAuth providers without an issuer of
|
|
59
|
+
* their own. OAuth identities use a distinct namespace so a provider ID
|
|
60
|
+
* cannot collide with an internal local authentication method.
|
|
61
|
+
*/
|
|
62
|
+
export function createOAuthAccountIssuer(providerId: string): string {
|
|
63
|
+
return `local:oauth:${encodeAccountIssuerProviderId(providerId)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
/**
|
|
41
67
|
* Account schema type used by better-auth, note that it's possible that account could have additional fields
|
|
42
68
|
*/
|
package/src/db/schema/user.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { coreSchema } from "./shared";
|
|
|
9
9
|
export const userSchema = coreSchema.extend({
|
|
10
10
|
// TODO(#9124): widen to nullish in v2. OAuth providers (Discord phone-only,
|
|
11
11
|
// Apple subsequent sign-ins, etc.) can legitimately omit email; identity
|
|
12
|
-
// must key on (
|
|
12
|
+
// must key on (issuer, providerAccountId) per OpenID Connect Core §5.7.
|
|
13
13
|
email: z.string().transform((val) => val.toLowerCase()),
|
|
14
14
|
emailVerified: z.boolean().default(false),
|
|
15
15
|
name: z.string(),
|
package/src/db/type.ts
CHANGED
|
@@ -281,6 +281,16 @@ export type DBFieldAttribute<T extends DBFieldType = DBFieldType> = {
|
|
|
281
281
|
type: T;
|
|
282
282
|
} & DBFieldAttributeConfig;
|
|
283
283
|
|
|
284
|
+
/** A database index spanning one or more logical schema fields. */
|
|
285
|
+
export interface DBTableIndex {
|
|
286
|
+
/** One to sixteen logical field names included in the index, in index order. */
|
|
287
|
+
fields: readonly [string, ...string[]];
|
|
288
|
+
/** Portable database index name of at most 63 UTF-8 bytes. */
|
|
289
|
+
name?: string | undefined;
|
|
290
|
+
/** Whether the indexed field tuple must be unique. */
|
|
291
|
+
unique?: boolean | undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
284
294
|
export type BetterAuthDBSchema = Record<
|
|
285
295
|
string,
|
|
286
296
|
{
|
|
@@ -292,6 +302,8 @@ export type BetterAuthDBSchema = Record<
|
|
|
292
302
|
* The fields of the table
|
|
293
303
|
*/
|
|
294
304
|
fields: Record<string, DBFieldAttribute>;
|
|
305
|
+
/** Table-level indexes, including compound indexes. */
|
|
306
|
+
indexes?: readonly DBTableIndex[] | undefined;
|
|
295
307
|
/**
|
|
296
308
|
* Whether to disable migrations for this table
|
|
297
309
|
* @default false
|
package/src/error/codes.ts
CHANGED
package/src/oauth2/index.ts
CHANGED
|
@@ -65,7 +65,10 @@ export {
|
|
|
65
65
|
export type {
|
|
66
66
|
OAuth2Tokens,
|
|
67
67
|
OAuth2UserInfo,
|
|
68
|
+
OAuthAccountKeyContext,
|
|
69
|
+
OAuthAccountSubject,
|
|
68
70
|
OAuthIdTokenConfig,
|
|
71
|
+
OAuthMappedUser,
|
|
69
72
|
OAuthProvider,
|
|
70
73
|
OAuthRefreshContext,
|
|
71
74
|
ProviderOptions,
|
|
@@ -97,7 +100,9 @@ export type {
|
|
|
97
100
|
VerifyAccessTokenRequestOptions,
|
|
98
101
|
} from "./verify";
|
|
99
102
|
export {
|
|
103
|
+
createInsufficientScopeError,
|
|
100
104
|
getJwks,
|
|
105
|
+
isInsufficientScopeError,
|
|
101
106
|
requestToResourceInput,
|
|
102
107
|
verifyAccessTokenRequest,
|
|
103
108
|
verifyBearerToken,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JWTVerifyGetKey } from "jose";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Awaitable,
|
|
4
|
+
GenericEndpointContext,
|
|
5
|
+
LiteralString,
|
|
6
|
+
} from "../types";
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* id_token verification config for a social provider.
|
|
@@ -51,7 +55,11 @@ export type OAuthIdTokenConfig =
|
|
|
51
55
|
* Custom verifier for providers that cannot verify against a local JWKS, such as a
|
|
52
56
|
* remote verification endpoint (e.g. LINE).
|
|
53
57
|
*/
|
|
54
|
-
verify: (
|
|
58
|
+
verify: (
|
|
59
|
+
token: string,
|
|
60
|
+
nonce?: string,
|
|
61
|
+
ctx?: GenericEndpointContext,
|
|
62
|
+
) => Promise<boolean>;
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
export interface OAuth2Tokens {
|
|
@@ -69,14 +77,52 @@ export interface OAuth2Tokens {
|
|
|
69
77
|
raw?: Record<string, unknown> | undefined;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/** Mutable local-user attributes normalized from an OAuth provider profile. */
|
|
72
81
|
export type OAuth2UserInfo = {
|
|
73
|
-
|
|
82
|
+
/** Provider identity belongs in raw profile data and `accountSubject`. */
|
|
83
|
+
id?: never;
|
|
74
84
|
name?: string | undefined;
|
|
75
85
|
email?: (string | null) | undefined;
|
|
76
86
|
image?: string | undefined;
|
|
77
87
|
emailVerified: boolean;
|
|
78
88
|
};
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Verified provider data available when deriving a stable OAuth account key.
|
|
92
|
+
*
|
|
93
|
+
* Account-key resolvers must use the raw provider profile or verified token
|
|
94
|
+
* response. They never receive the mapped local user, so profile mapping
|
|
95
|
+
* cannot redefine provider identity.
|
|
96
|
+
*/
|
|
97
|
+
export interface OAuthAccountKeyContext<Profile extends object = object> {
|
|
98
|
+
tokens: OAuth2Tokens;
|
|
99
|
+
profile: Profile;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolves one part of an account key from a profile returned by the same
|
|
104
|
+
* provider. The method-derived callback keeps that profile pairing intact when
|
|
105
|
+
* providers with different profile shapes share an `OAuthProvider[]`.
|
|
106
|
+
*/
|
|
107
|
+
type OAuthAccountKeyResolver<Profile extends object, Value> = {
|
|
108
|
+
resolve(context: OAuthAccountKeyContext<Profile>): Awaitable<Value>;
|
|
109
|
+
}["resolve"];
|
|
110
|
+
|
|
111
|
+
/** Resolves the stable provider subject used to build an OAuth account key. */
|
|
112
|
+
export type OAuthAccountSubject<Profile extends object = object> =
|
|
113
|
+
OAuthAccountKeyResolver<Profile, string | number>;
|
|
114
|
+
|
|
115
|
+
/** Mutable local-user attributes returned by `mapProfileToUser`. */
|
|
116
|
+
export type OAuthMappedUser = {
|
|
117
|
+
/** Provider identity is defined by `accountSubject`, not local user mapping. */
|
|
118
|
+
id?: never;
|
|
119
|
+
name?: string;
|
|
120
|
+
email?: string | null;
|
|
121
|
+
image?: string;
|
|
122
|
+
emailVerified?: boolean;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
};
|
|
125
|
+
|
|
80
126
|
/**
|
|
81
127
|
* Request metadata available to provider refresh hooks.
|
|
82
128
|
*
|
|
@@ -90,8 +136,8 @@ export interface OAuthRefreshContext {
|
|
|
90
136
|
}
|
|
91
137
|
|
|
92
138
|
export interface OAuthProvider<
|
|
93
|
-
T extends
|
|
94
|
-
O extends
|
|
139
|
+
T extends object = object,
|
|
140
|
+
O extends object = Partial<ProviderOptions>,
|
|
95
141
|
> {
|
|
96
142
|
id: LiteralString;
|
|
97
143
|
/**
|
|
@@ -127,6 +173,15 @@ export interface OAuthProvider<
|
|
|
127
173
|
additionalParams?: Record<string, string> | undefined;
|
|
128
174
|
}) => Awaitable<URL>;
|
|
129
175
|
name: string;
|
|
176
|
+
/**
|
|
177
|
+
* Stable subject that identifies the provider account.
|
|
178
|
+
*
|
|
179
|
+
* Read this value from the raw, provider-verified profile. For OpenID
|
|
180
|
+
* Connect providers, use the `sub` claim. For OAuth providers, use the
|
|
181
|
+
* provider's documented immutable user identifier. Never derive this value
|
|
182
|
+
* from `mapProfileToUser`.
|
|
183
|
+
*/
|
|
184
|
+
accountSubject: OAuthAccountSubject<T>;
|
|
130
185
|
validateAuthorizationCode: (data: {
|
|
131
186
|
code: string;
|
|
132
187
|
redirectURI: string;
|
|
@@ -173,6 +228,17 @@ export interface OAuthProvider<
|
|
|
173
228
|
) => Promise<OAuth2Tokens>)
|
|
174
229
|
| undefined;
|
|
175
230
|
revokeToken?: ((token: string) => Promise<void>) | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* Builds an OpenID Connect RP-Initiated Logout URL for this provider.
|
|
233
|
+
* Returns `null` when provider logout is unavailable or disabled.
|
|
234
|
+
*/
|
|
235
|
+
createEndSessionURL?:
|
|
236
|
+
| ((data: {
|
|
237
|
+
idToken?: string | null | undefined;
|
|
238
|
+
postLogoutRedirectURI?: string | undefined;
|
|
239
|
+
state?: string | undefined;
|
|
240
|
+
}) => Awaitable<URL | null>)
|
|
241
|
+
| undefined;
|
|
176
242
|
/**
|
|
177
243
|
* Declarative id_token verification config consumed by the shared
|
|
178
244
|
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
@@ -185,6 +251,16 @@ export interface OAuthProvider<
|
|
|
185
251
|
* against this value to prevent authorization server mix-up attacks.
|
|
186
252
|
*/
|
|
187
253
|
issuer?: string | undefined;
|
|
254
|
+
/**
|
|
255
|
+
* Stable issuer used with the provider subject to recognize an account.
|
|
256
|
+
*
|
|
257
|
+
* Use the validated OpenID Connect issuer for OIDC providers. A resolver is
|
|
258
|
+
* supported for tenant-specific issuers and receives only provider-verified
|
|
259
|
+
* data. OAuth providers without an issuer omit this property and are scoped
|
|
260
|
+
* to the synthetic `local:oauth:<encoded providerId>` issuer, where the
|
|
261
|
+
* provider ID segment is percent-encoded.
|
|
262
|
+
*/
|
|
263
|
+
accountIssuer?: string | OAuthAccountKeyResolver<T, string> | undefined;
|
|
188
264
|
/**
|
|
189
265
|
* Require shared OAuth redirect routes to bind ID-token verification to an
|
|
190
266
|
* authorization request nonce. When true, routes generate `idTokenNonce`,
|
|
@@ -218,7 +294,15 @@ export interface OAuthProvider<
|
|
|
218
294
|
options?: O | undefined;
|
|
219
295
|
}
|
|
220
296
|
|
|
221
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Maps a provider-specific profile while remaining compatible with the erased
|
|
299
|
+
* profile type used by shared OAuth helpers.
|
|
300
|
+
*/
|
|
301
|
+
type OAuthProfileMapper<Profile extends object> = {
|
|
302
|
+
map(profile: Profile): Awaitable<OAuthMappedUser>;
|
|
303
|
+
}["map"];
|
|
304
|
+
|
|
305
|
+
export type ProviderOptions<Profile extends object = object> = {
|
|
222
306
|
/**
|
|
223
307
|
* The client ID of your application.
|
|
224
308
|
*
|
|
@@ -262,25 +346,28 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
262
346
|
*/
|
|
263
347
|
disableIdTokenSignIn?: boolean | undefined;
|
|
264
348
|
/**
|
|
265
|
-
* verifyIdToken function to verify the id token
|
|
349
|
+
* verifyIdToken function to verify the id token.
|
|
350
|
+
*
|
|
351
|
+
* The optional endpoint context exposes request metadata to custom
|
|
352
|
+
* verifiers without coupling built-in provider verification to a runtime.
|
|
266
353
|
*/
|
|
267
354
|
verifyIdToken?:
|
|
268
|
-
| ((
|
|
355
|
+
| ((
|
|
356
|
+
token: string,
|
|
357
|
+
nonce?: string,
|
|
358
|
+
ctx?: GenericEndpointContext,
|
|
359
|
+
) => Promise<boolean>)
|
|
269
360
|
| undefined;
|
|
270
361
|
/**
|
|
271
362
|
* Custom function to get user info from the provider
|
|
363
|
+
*
|
|
364
|
+
* `data` must preserve the declared raw profile shape because account-key
|
|
365
|
+
* resolvers consume it after this hook returns.
|
|
272
366
|
*/
|
|
273
367
|
getUserInfo?:
|
|
274
368
|
| ((token: OAuth2Tokens) => Promise<{
|
|
275
|
-
user:
|
|
276
|
-
|
|
277
|
-
name?: string;
|
|
278
|
-
email?: string | null;
|
|
279
|
-
image?: string;
|
|
280
|
-
emailVerified: boolean;
|
|
281
|
-
[key: string]: any;
|
|
282
|
-
};
|
|
283
|
-
data: any;
|
|
369
|
+
user: OAuth2UserInfo & Record<string, unknown>;
|
|
370
|
+
data: Profile;
|
|
284
371
|
} | null>)
|
|
285
372
|
| undefined;
|
|
286
373
|
/**
|
|
@@ -293,25 +380,7 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
293
380
|
* Custom function to map the provider profile to a
|
|
294
381
|
* user.
|
|
295
382
|
*/
|
|
296
|
-
mapProfileToUser?:
|
|
297
|
-
| ((profile: Profile) =>
|
|
298
|
-
| {
|
|
299
|
-
id?: string;
|
|
300
|
-
name?: string;
|
|
301
|
-
email?: string | null;
|
|
302
|
-
image?: string;
|
|
303
|
-
emailVerified?: boolean;
|
|
304
|
-
[key: string]: any;
|
|
305
|
-
}
|
|
306
|
-
| Promise<{
|
|
307
|
-
id?: string;
|
|
308
|
-
name?: string;
|
|
309
|
-
email?: string | null;
|
|
310
|
-
image?: string;
|
|
311
|
-
emailVerified?: boolean;
|
|
312
|
-
[key: string]: any;
|
|
313
|
-
}>)
|
|
314
|
-
| undefined;
|
|
383
|
+
mapProfileToUser?: OAuthProfileMapper<Profile> | undefined;
|
|
315
384
|
/**
|
|
316
385
|
* Disable implicit sign up for new users. When set to true for the provider,
|
|
317
386
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decodeProtectedHeader, jwtVerify } from "jose";
|
|
2
|
+
import type { GenericEndpointContext } from "../types";
|
|
2
3
|
import type { OAuthProvider, ProviderOptions } from "./oauth-provider";
|
|
3
4
|
|
|
4
5
|
type ProviderWithIdTokenConfig = Pick<OAuthProvider, "idToken" | "options">;
|
|
@@ -59,6 +60,7 @@ export async function verifyProviderIdToken(
|
|
|
59
60
|
provider: ProviderWithIdTokenConfig,
|
|
60
61
|
token: string,
|
|
61
62
|
nonce?: string,
|
|
63
|
+
ctx?: GenericEndpointContext,
|
|
62
64
|
): Promise<boolean> {
|
|
63
65
|
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
64
66
|
if (options.disableIdTokenSignIn) {
|
|
@@ -69,14 +71,14 @@ export async function verifyProviderIdToken(
|
|
|
69
71
|
// escaping to the caller as a server error.
|
|
70
72
|
try {
|
|
71
73
|
if (options.verifyIdToken) {
|
|
72
|
-
return await options.verifyIdToken(token, nonce);
|
|
74
|
+
return await options.verifyIdToken(token, nonce, ctx);
|
|
73
75
|
}
|
|
74
76
|
const config = provider.idToken;
|
|
75
77
|
if (!config) {
|
|
76
78
|
return false;
|
|
77
79
|
}
|
|
78
80
|
if ("verify" in config) {
|
|
79
|
-
return await config.verify(token, nonce);
|
|
81
|
+
return await config.verify(token, nonce, ctx);
|
|
80
82
|
}
|
|
81
83
|
// Opaque (non-JWS) tokens carry no signature to check. They are accepted only when the
|
|
82
84
|
// provider opts in, in which case getUserInfo resolves identity from the access token via
|
package/src/oauth2/verify.ts
CHANGED
|
@@ -164,8 +164,16 @@ export interface VerifyAccessTokenOptions {
|
|
|
164
164
|
/** Verify options */
|
|
165
165
|
verifyOptions: JWTVerifyOptions &
|
|
166
166
|
Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
|
|
167
|
-
/** Scopes
|
|
168
|
-
|
|
167
|
+
/** Scopes the token must satisfy. */
|
|
168
|
+
requiredScopes?: readonly string[];
|
|
169
|
+
/**
|
|
170
|
+
* Determines whether a required scope is satisfied by the granted scope
|
|
171
|
+
* set. Defaults to exact set membership.
|
|
172
|
+
*/
|
|
173
|
+
isScopeSatisfied?: (
|
|
174
|
+
requiredScope: string,
|
|
175
|
+
grantedScopes: ReadonlySet<string>,
|
|
176
|
+
) => boolean;
|
|
169
177
|
/** Required to verify access token locally */
|
|
170
178
|
jwksUrl?: string;
|
|
171
179
|
/** If provided, can verify a token remotely */
|
|
@@ -442,23 +450,122 @@ async function verifyAccessTokenPayload(
|
|
|
442
450
|
message: `no token payload`,
|
|
443
451
|
});
|
|
444
452
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
const grantedScopes = parseGrantedScopes(payload.scope);
|
|
454
|
+
|
|
455
|
+
// Check scopes if provided.
|
|
456
|
+
if (opts.requiredScopes) {
|
|
457
|
+
const isScopeSatisfied =
|
|
458
|
+
opts.isScopeSatisfied ??
|
|
459
|
+
((requiredScope: string, scopes: ReadonlySet<string>) =>
|
|
460
|
+
scopes.has(requiredScope));
|
|
461
|
+
// RFC 6750 §3.1: report every missing scope at once. Challenging with one
|
|
462
|
+
// scope at a time costs the user a browser round-trip per scope.
|
|
463
|
+
const missingScopes = opts.requiredScopes.filter(
|
|
464
|
+
(scope) => !isScopeSatisfied(scope, grantedScopes),
|
|
449
465
|
);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
throw new APIError("FORBIDDEN", {
|
|
453
|
-
message: `invalid scope ${sc}`,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
466
|
+
if (missingScopes.length > 0) {
|
|
467
|
+
throw createInsufficientScopeError(missingScopes);
|
|
456
468
|
}
|
|
457
469
|
}
|
|
458
470
|
|
|
459
471
|
return payload;
|
|
460
472
|
}
|
|
461
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Build the RFC 6750 §3.1 insufficient-scope failure: the access token is valid
|
|
476
|
+
* but lacks scopes the operation needs.
|
|
477
|
+
*
|
|
478
|
+
* Resource-server challenge builders turn this into a `403` carrying a
|
|
479
|
+
* `WWW-Authenticate: Bearer error="insufficient_scope"` challenge that names
|
|
480
|
+
* `scopes`, so the client knows what to request when it re-authorizes. Throw it
|
|
481
|
+
* from a route handler to challenge for scopes only that operation needs; a
|
|
482
|
+
* plain `FORBIDDEN` stays a plain `403`, since a permission denial the client
|
|
483
|
+
* cannot fix by re-authorizing must not send the user through consent again.
|
|
484
|
+
*
|
|
485
|
+
* @param requiredScopes - Every scope the operation requires but the token lacks.
|
|
486
|
+
* @param description - RFC 6750 `error_description` text. It must use the
|
|
487
|
+
* printable ASCII character set allowed by the specification.
|
|
488
|
+
*/
|
|
489
|
+
const OAUTH_SCOPE_TOKEN_PATTERN = /^[\x21\x23-\x5b\x5d-\x7e]+$/;
|
|
490
|
+
const OAUTH_ERROR_DESCRIPTION_PATTERN = /^[\x20-\x21\x23-\x5b\x5d-\x7e]+$/;
|
|
491
|
+
const insufficientScopeErrors = new WeakSet<APIError>();
|
|
492
|
+
|
|
493
|
+
function isOAuthScopeToken(value: string): boolean {
|
|
494
|
+
return OAUTH_SCOPE_TOKEN_PATTERN.test(value);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function validateScopeTokens(scopes: readonly string[], label: string): void {
|
|
498
|
+
for (const scope of scopes) {
|
|
499
|
+
if (!isOAuthScopeToken(scope)) {
|
|
500
|
+
throw new TypeError(`invalid ${label}: ${JSON.stringify(scope)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function validateRequiredScopes(opts: VerifyAccessTokenOptions): void {
|
|
506
|
+
if (opts.requiredScopes) {
|
|
507
|
+
validateScopeTokens(opts.requiredScopes, "required scope");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function parseGrantedScopes(scope: unknown): ReadonlySet<string> {
|
|
512
|
+
if (scope === undefined) return new Set();
|
|
513
|
+
if (
|
|
514
|
+
typeof scope !== "string" ||
|
|
515
|
+
scope.length === 0 ||
|
|
516
|
+
scope.split(" ").some((token) => !isOAuthScopeToken(token))
|
|
517
|
+
) {
|
|
518
|
+
throw new APIError("UNAUTHORIZED", {
|
|
519
|
+
message: "access token scope claim is invalid",
|
|
520
|
+
error: "invalid_token",
|
|
521
|
+
error_description: "access token scope claim is invalid",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return new Set(scope.split(" "));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function createInsufficientScopeError(
|
|
528
|
+
requiredScopes: readonly string[],
|
|
529
|
+
description = `access token is missing required scope: ${requiredScopes.join(" ")}`,
|
|
530
|
+
): APIError {
|
|
531
|
+
if (requiredScopes.length === 0) {
|
|
532
|
+
throw new TypeError("requiredScopes must contain at least one scope");
|
|
533
|
+
}
|
|
534
|
+
validateScopeTokens(requiredScopes, "required scope");
|
|
535
|
+
if (
|
|
536
|
+
typeof description !== "string" ||
|
|
537
|
+
!OAUTH_ERROR_DESCRIPTION_PATTERN.test(description)
|
|
538
|
+
) {
|
|
539
|
+
throw new TypeError("invalid error_description");
|
|
540
|
+
}
|
|
541
|
+
const error = new APIError("FORBIDDEN", {
|
|
542
|
+
message: description,
|
|
543
|
+
error: "insufficient_scope",
|
|
544
|
+
error_description: description,
|
|
545
|
+
scope: [...new Set(requiredScopes)].join(" "),
|
|
546
|
+
});
|
|
547
|
+
insufficientScopeErrors.add(error);
|
|
548
|
+
return error;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Returns whether an error is a typed RFC 6750 insufficient-scope failure.
|
|
553
|
+
*/
|
|
554
|
+
export function isInsufficientScopeError(error: unknown): error is APIError {
|
|
555
|
+
if (
|
|
556
|
+
!(error instanceof APIError) ||
|
|
557
|
+
!insufficientScopeErrors.has(error) ||
|
|
558
|
+
error.status !== "FORBIDDEN"
|
|
559
|
+
)
|
|
560
|
+
return false;
|
|
561
|
+
const body = error.body as { error?: unknown; scope?: unknown } | undefined;
|
|
562
|
+
if (body?.error !== "insufficient_scope" || typeof body.scope !== "string") {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const scopes = body.scope.split(" ");
|
|
566
|
+
return scopes.length > 0 && scopes.every(isOAuthScopeToken);
|
|
567
|
+
}
|
|
568
|
+
|
|
462
569
|
function throwDpopUnauthorized(
|
|
463
570
|
message: string,
|
|
464
571
|
error?: "invalid_dpop_proof" | "invalid_token",
|
|
@@ -489,6 +596,7 @@ export async function verifyBearerToken(
|
|
|
489
596
|
token: string,
|
|
490
597
|
opts: VerifyAccessTokenOptions,
|
|
491
598
|
) {
|
|
599
|
+
validateRequiredScopes(opts);
|
|
492
600
|
const payload = await verifyAccessTokenPayload(token, opts);
|
|
493
601
|
if (getDpopJktFromPayload(payload)) {
|
|
494
602
|
throwDpopUnauthorized(
|
|
@@ -512,6 +620,7 @@ export async function verifyAccessTokenRequest(
|
|
|
512
620
|
request: ResourceRequestInput,
|
|
513
621
|
opts: VerifyAccessTokenRequestOptions,
|
|
514
622
|
) {
|
|
623
|
+
validateRequiredScopes(opts);
|
|
515
624
|
const authorization = parseAccessTokenAuthorization(
|
|
516
625
|
request.authorizationHeader,
|
|
517
626
|
);
|
|
@@ -82,6 +82,8 @@ export const apple = (options: AppleOptions) => {
|
|
|
82
82
|
return {
|
|
83
83
|
id: "apple",
|
|
84
84
|
name: "Apple",
|
|
85
|
+
accountSubject: ({ profile }) => profile.sub,
|
|
86
|
+
accountIssuer: "https://appleid.apple.com",
|
|
85
87
|
async createAuthorizationURL({
|
|
86
88
|
state,
|
|
87
89
|
scopes,
|
|
@@ -174,7 +176,6 @@ export const apple = (options: AppleOptions) => {
|
|
|
174
176
|
const userMap = await options.mapProfileToUser?.(enrichedProfile);
|
|
175
177
|
return {
|
|
176
178
|
user: {
|
|
177
|
-
id: profile.sub,
|
|
178
179
|
name: enrichedProfile.name,
|
|
179
180
|
emailVerified: emailVerified,
|
|
180
181
|
email: profile.email,
|
|
@@ -34,6 +34,7 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
34
34
|
return {
|
|
35
35
|
id: "atlassian",
|
|
36
36
|
name: "Atlassian",
|
|
37
|
+
accountSubject: ({ profile }) => profile.account_id,
|
|
37
38
|
|
|
38
39
|
async createAuthorizationURL({
|
|
39
40
|
state,
|
|
@@ -120,7 +121,6 @@ export const atlassian = (options: AtlassianOptions) => {
|
|
|
120
121
|
|
|
121
122
|
return {
|
|
122
123
|
user: {
|
|
123
|
-
id: profile.account_id,
|
|
124
124
|
name: profile.name,
|
|
125
125
|
email: profile.email,
|
|
126
126
|
image: profile.picture,
|
|
@@ -73,6 +73,8 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
73
73
|
return {
|
|
74
74
|
id: "cognito",
|
|
75
75
|
name: "Cognito",
|
|
76
|
+
accountSubject: ({ profile }) => profile.sub,
|
|
77
|
+
accountIssuer: `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`,
|
|
76
78
|
async createAuthorizationURL({
|
|
77
79
|
state,
|
|
78
80
|
scopes,
|
|
@@ -184,7 +186,6 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
184
186
|
|
|
185
187
|
return {
|
|
186
188
|
user: {
|
|
187
|
-
id: profile.sub,
|
|
188
189
|
name: enrichedProfile.name,
|
|
189
190
|
email: profile.email,
|
|
190
191
|
image: profile.picture,
|
|
@@ -213,7 +214,6 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
213
214
|
const userMap = await options.mapProfileToUser?.(userInfo);
|
|
214
215
|
return {
|
|
215
216
|
user: {
|
|
216
|
-
id: userInfo.sub,
|
|
217
217
|
name:
|
|
218
218
|
userInfo.name ||
|
|
219
219
|
userInfo.given_name ||
|
|
@@ -88,6 +88,7 @@ export const discord = (options: DiscordOptions) => {
|
|
|
88
88
|
return {
|
|
89
89
|
id: "discord",
|
|
90
90
|
name: "Discord",
|
|
91
|
+
accountSubject: ({ profile }) => profile.id,
|
|
91
92
|
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
|
92
93
|
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
|
|
93
94
|
if (scopes) _scopes.push(...scopes);
|
|
@@ -159,7 +160,6 @@ export const discord = (options: DiscordOptions) => {
|
|
|
159
160
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
160
161
|
return {
|
|
161
162
|
user: {
|
|
162
|
-
id: profile.id,
|
|
163
163
|
name: profile.global_name || profile.username || "",
|
|
164
164
|
email: profile.email,
|
|
165
165
|
emailVerified: profile.verified,
|
|
@@ -31,6 +31,7 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
31
31
|
return {
|
|
32
32
|
id: "dropbox",
|
|
33
33
|
name: "Dropbox",
|
|
34
|
+
accountSubject: ({ profile }) => profile.account_id,
|
|
34
35
|
createAuthorizationURL: async ({
|
|
35
36
|
state,
|
|
36
37
|
scopes,
|
|
@@ -99,7 +100,6 @@ export const dropbox = (options: DropboxOptions) => {
|
|
|
99
100
|
const userMap = await options.mapProfileToUser?.(profile);
|
|
100
101
|
return {
|
|
101
102
|
user: {
|
|
102
|
-
id: profile.account_id,
|
|
103
103
|
name: profile.name?.display_name,
|
|
104
104
|
email: profile.email,
|
|
105
105
|
emailVerified: profile.email_verified || false,
|