@better-auth/core 1.6.11 → 1.6.12
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/context/global.mjs +1 -1
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/utils.d.mts +9 -1
- package/dist/oauth2/utils.mjs +12 -1
- package/dist/oauth2/verify.mjs +15 -5
- package/dist/social-providers/apple.mjs +11 -1
- package/dist/types/context.d.mts +5 -3
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/package.json +5 -5
- package/src/oauth2/index.ts +1 -0
- package/src/oauth2/utils.ts +19 -0
- package/src/oauth2/verify.ts +20 -4
- package/src/social-providers/apple.ts +19 -1
- package/src/types/context.ts +5 -3
- package/src/utils/string.ts +37 -0
package/dist/context/global.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes.mjs";
|
|
|
2
2
|
import { getOpenTelemetryAPI } from "./api.mjs";
|
|
3
3
|
//#region src/instrumentation/tracer.ts
|
|
4
4
|
const INSTRUMENTATION_SCOPE = "better-auth";
|
|
5
|
-
const INSTRUMENTATION_VERSION = "1.6.
|
|
5
|
+
const INSTRUMENTATION_VERSION = "1.6.12";
|
|
6
6
|
/**
|
|
7
7
|
* Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
|
|
8
8
|
* callbacks). These are APIErrors with 3xx status codes and should not be
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { OAuth2Tokens, OAuth2UserInfo, OAuthProvider, ProviderOptions } from "./
|
|
|
2
2
|
import { clientCredentialsToken, clientCredentialsTokenRequest, createClientCredentialsTokenRequest } from "./client-credentials-token.mjs";
|
|
3
3
|
import { createAuthorizationURL } from "./create-authorization-url.mjs";
|
|
4
4
|
import { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
|
|
5
|
-
import { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
5
|
+
import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
6
6
|
import { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
|
|
7
7
|
import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
|
|
8
|
-
export { type OAuth2Tokens, type OAuth2UserInfo, type OAuthProvider, type ProviderOptions, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
|
|
8
|
+
export { type OAuth2Tokens, type OAuth2UserInfo, type OAuthProvider, type ProviderOptions, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clientCredentialsToken, clientCredentialsTokenRequest, createClientCredentialsTokenRequest } from "./client-credentials-token.mjs";
|
|
2
|
-
import { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
2
|
+
import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
3
3
|
import { createAuthorizationURL } from "./create-authorization-url.mjs";
|
|
4
4
|
import { createRefreshAccessTokenRequest, refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
|
|
5
5
|
import { authorizationCodeRequest, createAuthorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
|
|
6
6
|
import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
|
|
7
|
-
export { authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
|
|
7
|
+
export { applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationCodeRequest, createAuthorizationURL, createClientCredentialsTokenRequest, createRefreshAccessTokenRequest, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, refreshAccessToken, refreshAccessTokenRequest, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken };
|
package/dist/oauth2/utils.d.mts
CHANGED
|
@@ -2,6 +2,14 @@ import { OAuth2Tokens } from "./oauth-provider.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/oauth2/utils.d.ts
|
|
4
4
|
declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
|
|
5
|
+
/**
|
|
6
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
7
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
8
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
9
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
10
|
+
* fallback is configured.
|
|
11
|
+
*/
|
|
12
|
+
declare function applyDefaultAccessTokenExpiry(tokens: OAuth2Tokens, accessTokenExpiresIn: number | undefined): OAuth2Tokens;
|
|
5
13
|
/**
|
|
6
14
|
* Return the provider's primary Client ID: the single string, or the entry at
|
|
7
15
|
* array index 0 for the cross-platform form used by ID token audience
|
|
@@ -13,4 +21,4 @@ declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
|
|
|
13
21
|
declare function getPrimaryClientId(clientId: unknown): string | undefined;
|
|
14
22
|
declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
|
|
15
23
|
//#endregion
|
|
16
|
-
export { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
|
|
24
|
+
export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
|
package/dist/oauth2/utils.mjs
CHANGED
|
@@ -17,6 +17,17 @@ function getOAuth2Tokens(data) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
21
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
22
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
23
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
24
|
+
* fallback is configured.
|
|
25
|
+
*/
|
|
26
|
+
function applyDefaultAccessTokenExpiry(tokens, accessTokenExpiresIn) {
|
|
27
|
+
if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) tokens.accessTokenExpiresAt = new Date(Date.now() + accessTokenExpiresIn * 1e3);
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
20
31
|
* Return the provider's primary Client ID: the single string, or the entry at
|
|
21
32
|
* array index 0 for the cross-platform form used by ID token audience
|
|
22
33
|
* verification. Index 0 is the designated primary and pairs with
|
|
@@ -34,4 +45,4 @@ async function generateCodeChallenge(codeVerifier) {
|
|
|
34
45
|
return base64Url.encode(new Uint8Array(hash), { padding: false });
|
|
35
46
|
}
|
|
36
47
|
//#endregion
|
|
37
|
-
export { generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
|
|
48
|
+
export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId };
|
package/dist/oauth2/verify.mjs
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { logger } from "../env/logger.mjs";
|
|
2
2
|
import { APIError } from "better-call";
|
|
3
3
|
import { betterFetch } from "@better-fetch/fetch";
|
|
4
|
-
import { UnsecuredJWT, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
4
|
+
import { UnsecuredJWT, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
|
|
5
5
|
//#region src/oauth2/verify.ts
|
|
6
|
+
const joseInfrastructureErrorCodes = new Set([
|
|
7
|
+
errors.JWKSTimeout.code,
|
|
8
|
+
errors.JWKSInvalid.code,
|
|
9
|
+
errors.JWKSMultipleMatchingKeys.code
|
|
10
|
+
]);
|
|
11
|
+
function isJoseInfrastructureError(error) {
|
|
12
|
+
return joseInfrastructureErrorCodes.has(error.code);
|
|
13
|
+
}
|
|
6
14
|
/** Last fetched jwks used locally in getJwks @internal */
|
|
7
15
|
let jwks;
|
|
8
16
|
/**
|
|
@@ -28,7 +36,7 @@ async function getJwks(token, opts) {
|
|
|
28
36
|
if (error instanceof Error) throw error;
|
|
29
37
|
throw new Error(error);
|
|
30
38
|
}
|
|
31
|
-
if (!jwtHeaders.kid) throw new
|
|
39
|
+
if (!jwtHeaders.kid) throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
|
|
32
40
|
if (!jwks || !jwks.keys.find((jwk) => jwk.kid === jwtHeaders.kid)) {
|
|
33
41
|
jwks = typeof opts.jwksFetch === "string" ? await betterFetch(opts.jwksFetch, { headers: { Accept: "application/json" } }).then(async (res) => {
|
|
34
42
|
if (res.error) throw new Error(`Jwks failed: ${res.error.message ?? res.error.statusText}`);
|
|
@@ -51,9 +59,11 @@ async function verifyAccessToken(token, opts) {
|
|
|
51
59
|
verifyOptions: opts.verifyOptions
|
|
52
60
|
});
|
|
53
61
|
} catch (error) {
|
|
54
|
-
if (error instanceof Error) if (error.name === "TypeError" || error.name === "JWSInvalid") {} else if (error
|
|
55
|
-
else if (error
|
|
56
|
-
|
|
62
|
+
if (error instanceof Error) if (error.name === "TypeError" || error.name === "JWSInvalid") {} else if (error instanceof errors.JWTExpired) throw new APIError("UNAUTHORIZED", { message: "token expired" });
|
|
63
|
+
else if (error instanceof errors.JOSEError) {
|
|
64
|
+
if (isJoseInfrastructureError(error)) throw error;
|
|
65
|
+
throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
|
|
66
|
+
} else throw error;
|
|
57
67
|
else throw new Error(error);
|
|
58
68
|
}
|
|
59
69
|
if (opts?.remoteVerify) {
|
|
@@ -7,6 +7,16 @@ import { validateAuthorizationCode } from "../oauth2/validate-authorization-code
|
|
|
7
7
|
import { betterFetch } from "@better-fetch/fetch";
|
|
8
8
|
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
9
9
|
//#region src/social-providers/apple.ts
|
|
10
|
+
async function sha256Hex(value) {
|
|
11
|
+
const data = new TextEncoder().encode(value);
|
|
12
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
13
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
14
|
+
}
|
|
15
|
+
async function nonceMatches(jwtNonce, nonce) {
|
|
16
|
+
if (typeof jwtNonce !== "string") return false;
|
|
17
|
+
if (jwtNonce === nonce) return true;
|
|
18
|
+
return jwtNonce === await sha256Hex(nonce);
|
|
19
|
+
}
|
|
10
20
|
const apple = (options) => {
|
|
11
21
|
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
12
22
|
return {
|
|
@@ -55,7 +65,7 @@ const apple = (options) => {
|
|
|
55
65
|
["email_verified", "is_private_email"].forEach((field) => {
|
|
56
66
|
if (jwtClaims[field] !== void 0) jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
57
67
|
});
|
|
58
|
-
if (nonce && jwtClaims.nonce
|
|
68
|
+
if (nonce && !await nonceMatches(jwtClaims.nonce, nonce)) return false;
|
|
59
69
|
return !!jwtClaims;
|
|
60
70
|
} catch {
|
|
61
71
|
return false;
|
package/dist/types/context.d.mts
CHANGED
|
@@ -118,9 +118,11 @@ interface InternalAdapter<_Options extends BetterAuthOptions = BetterAuthOptions
|
|
|
118
118
|
* Atomically consume a single-use verification row by `identifier` and
|
|
119
119
|
* return it. Only the first concurrent caller receives the latest row;
|
|
120
120
|
* subsequent callers receive `null`. Consuming one row invalidates the
|
|
121
|
-
* whole identifier so stale rows cannot be replayed.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* whole identifier so stale rows cannot be replayed. Rows past their
|
|
122
|
+
* `expiresAt` are treated as already invalid: the row is deleted but
|
|
123
|
+
* `null` is returned, so callers do not need to gate on `expiresAt`
|
|
124
|
+
* themselves. Callers MUST gate any state change (issue session, mint
|
|
125
|
+
* token, change password) on a non-null result.
|
|
124
126
|
*
|
|
125
127
|
* Replaces the racy `findVerificationValue` + `deleteVerificationByIdentifier`
|
|
126
128
|
* pair at single-use credential consumption sites.
|
package/dist/utils/string.d.mts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/utils/string.d.ts
|
|
2
2
|
declare function capitalizeFirstLetter(str: string): string;
|
|
3
|
+
declare function toSnakeCase(input: string): string;
|
|
4
|
+
declare function toKebabCase(input: string): string;
|
|
5
|
+
declare function toCamelCase(input: string): string;
|
|
6
|
+
declare function toPascalCase(input: string): string;
|
|
3
7
|
//#endregion
|
|
4
|
-
export { capitalizeFirstLetter };
|
|
8
|
+
export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
|
package/dist/utils/string.mjs
CHANGED
|
@@ -2,5 +2,24 @@
|
|
|
2
2
|
function capitalizeFirstLetter(str) {
|
|
3
3
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4
4
|
}
|
|
5
|
+
const WORD_PATTERN = /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
|
|
6
|
+
const APOSTROPHE_PATTERN = /['\u2019]/g;
|
|
7
|
+
function splitWords(input) {
|
|
8
|
+
return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
|
|
9
|
+
}
|
|
10
|
+
function toSnakeCase(input) {
|
|
11
|
+
return splitWords(input).map((word) => word.toLowerCase()).join("_");
|
|
12
|
+
}
|
|
13
|
+
function toKebabCase(input) {
|
|
14
|
+
return splitWords(input).map((word) => word.toLowerCase()).join("-");
|
|
15
|
+
}
|
|
16
|
+
function toCamelCase(input) {
|
|
17
|
+
return splitWords(input).reduce((acc, word, i) => {
|
|
18
|
+
return acc + (i === 0 ? word.toLowerCase() : `${word[0].toUpperCase()}${word.slice(1)}`);
|
|
19
|
+
}, "");
|
|
20
|
+
}
|
|
21
|
+
function toPascalCase(input) {
|
|
22
|
+
return splitWords(input).map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
|
|
23
|
+
}
|
|
5
24
|
//#endregion
|
|
6
|
-
export { capitalizeFirstLetter };
|
|
25
|
+
export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.12",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"zod": "^4.3.6"
|
|
153
153
|
},
|
|
154
154
|
"devDependencies": {
|
|
155
|
-
"@better-auth/utils": "0.4.
|
|
155
|
+
"@better-auth/utils": "0.4.1",
|
|
156
156
|
"@better-fetch/fetch": "1.1.21",
|
|
157
157
|
"@opentelemetry/api": "^1.9.0",
|
|
158
158
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
@@ -160,18 +160,18 @@
|
|
|
160
160
|
"better-call": "1.3.5",
|
|
161
161
|
"@cloudflare/workers-types": "^4.20250121.0",
|
|
162
162
|
"jose": "^6.1.3",
|
|
163
|
-
"kysely": "^0.28.17",
|
|
163
|
+
"kysely": "^0.28.17 || ^0.29.0",
|
|
164
164
|
"nanostores": "^1.1.1",
|
|
165
165
|
"tsdown": "0.21.1"
|
|
166
166
|
},
|
|
167
167
|
"peerDependencies": {
|
|
168
|
-
"@better-auth/utils": "0.4.
|
|
168
|
+
"@better-auth/utils": "0.4.1",
|
|
169
169
|
"@better-fetch/fetch": "1.1.21",
|
|
170
170
|
"@opentelemetry/api": "^1.9.0",
|
|
171
171
|
"better-call": "1.3.5",
|
|
172
172
|
"@cloudflare/workers-types": ">=4",
|
|
173
173
|
"jose": "^6.1.0",
|
|
174
|
-
"kysely": "^0.28.5",
|
|
174
|
+
"kysely": "^0.28.5 || ^0.29.0",
|
|
175
175
|
"nanostores": "^1.0.1"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
package/src/oauth2/index.ts
CHANGED
package/src/oauth2/utils.ts
CHANGED
|
@@ -28,6 +28,25 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
33
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
34
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
35
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
36
|
+
* fallback is configured.
|
|
37
|
+
*/
|
|
38
|
+
export function applyDefaultAccessTokenExpiry(
|
|
39
|
+
tokens: OAuth2Tokens,
|
|
40
|
+
accessTokenExpiresIn: number | undefined,
|
|
41
|
+
): OAuth2Tokens {
|
|
42
|
+
if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) {
|
|
43
|
+
tokens.accessTokenExpiresAt = new Date(
|
|
44
|
+
Date.now() + accessTokenExpiresIn * 1000,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return tokens;
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/**
|
|
32
51
|
* Return the provider's primary Client ID: the single string, or the entry at
|
|
33
52
|
* array index 0 for the cross-platform form used by ID token audience
|
package/src/oauth2/verify.ts
CHANGED
|
@@ -9,11 +9,22 @@ 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
|
|
|
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
|
+
|
|
17
28
|
/** Last fetched jwks used locally in getJwks @internal */
|
|
18
29
|
let jwks: JSONWebKeySet | undefined;
|
|
19
30
|
|
|
@@ -82,7 +93,9 @@ export async function getJwks(
|
|
|
82
93
|
throw new Error(error as unknown as string);
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
if (!jwtHeaders.kid)
|
|
96
|
+
if (!jwtHeaders.kid) {
|
|
97
|
+
throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
|
|
98
|
+
}
|
|
86
99
|
|
|
87
100
|
// Fetch jwks if not set or has a different kid than the one stored
|
|
88
101
|
if (!jwks || !jwks.keys.find((jwk) => jwk.kid === jwtHeaders.kid)) {
|
|
@@ -137,13 +150,16 @@ export async function verifyAccessToken(
|
|
|
137
150
|
if (error instanceof Error) {
|
|
138
151
|
if (error.name === "TypeError" || error.name === "JWSInvalid") {
|
|
139
152
|
// likely an opaque token (continue)
|
|
140
|
-
} else if (error
|
|
153
|
+
} else if (error instanceof joseErrors.JWTExpired) {
|
|
141
154
|
throw new APIError("UNAUTHORIZED", {
|
|
142
155
|
message: "token expired",
|
|
143
156
|
});
|
|
144
|
-
} else if (error
|
|
157
|
+
} else if (error instanceof joseErrors.JOSEError) {
|
|
158
|
+
if (isJoseInfrastructureError(error)) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
145
161
|
throw new APIError("UNAUTHORIZED", {
|
|
146
|
-
message: "token
|
|
162
|
+
message: "invalid access token",
|
|
147
163
|
});
|
|
148
164
|
} else {
|
|
149
165
|
throw error;
|
|
@@ -77,6 +77,24 @@ export interface AppleOptions extends ProviderOptions<AppleProfile> {
|
|
|
77
77
|
audience?: (string | string[]) | undefined;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
async function sha256Hex(value: string) {
|
|
81
|
+
const data = new TextEncoder().encode(value);
|
|
82
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
83
|
+
return Array.from(new Uint8Array(digest))
|
|
84
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
85
|
+
.join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function nonceMatches(jwtNonce: unknown, nonce: string) {
|
|
89
|
+
if (typeof jwtNonce !== "string") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (jwtNonce === nonce) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return jwtNonce === (await sha256Hex(nonce));
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
export const apple = (options: AppleOptions) => {
|
|
81
99
|
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
82
100
|
return {
|
|
@@ -141,7 +159,7 @@ export const apple = (options: AppleOptions) => {
|
|
|
141
159
|
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
142
160
|
}
|
|
143
161
|
});
|
|
144
|
-
if (nonce && jwtClaims.nonce
|
|
162
|
+
if (nonce && !(await nonceMatches(jwtClaims.nonce, nonce))) {
|
|
145
163
|
return false;
|
|
146
164
|
}
|
|
147
165
|
return !!jwtClaims;
|
package/src/types/context.ts
CHANGED
|
@@ -220,9 +220,11 @@ export interface InternalAdapter<
|
|
|
220
220
|
* Atomically consume a single-use verification row by `identifier` and
|
|
221
221
|
* return it. Only the first concurrent caller receives the latest row;
|
|
222
222
|
* subsequent callers receive `null`. Consuming one row invalidates the
|
|
223
|
-
* whole identifier so stale rows cannot be replayed.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* whole identifier so stale rows cannot be replayed. Rows past their
|
|
224
|
+
* `expiresAt` are treated as already invalid: the row is deleted but
|
|
225
|
+
* `null` is returned, so callers do not need to gate on `expiresAt`
|
|
226
|
+
* themselves. Callers MUST gate any state change (issue session, mint
|
|
227
|
+
* token, change password) on a non-null result.
|
|
226
228
|
*
|
|
227
229
|
* Replaces the racy `findVerificationValue` + `deleteVerificationByIdentifier`
|
|
228
230
|
* pair at single-use credential consumption sites.
|
package/src/utils/string.ts
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
1
|
export function capitalizeFirstLetter(str: string) {
|
|
2
2
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
const WORD_PATTERN =
|
|
6
|
+
/[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
|
|
7
|
+
const APOSTROPHE_PATTERN = /['\u2019]/g;
|
|
8
|
+
|
|
9
|
+
function splitWords(input: string): string[] {
|
|
10
|
+
return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toSnakeCase(input: string): string {
|
|
14
|
+
return splitWords(input)
|
|
15
|
+
.map((word) => word.toLowerCase())
|
|
16
|
+
.join("_");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toKebabCase(input: string): string {
|
|
20
|
+
return splitWords(input)
|
|
21
|
+
.map((word) => word.toLowerCase())
|
|
22
|
+
.join("-");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toCamelCase(input: string): string {
|
|
26
|
+
return splitWords(input).reduce((acc, word, i) => {
|
|
27
|
+
return (
|
|
28
|
+
acc +
|
|
29
|
+
(i === 0
|
|
30
|
+
? word.toLowerCase()
|
|
31
|
+
: `${word[0]!.toUpperCase()}${word.slice(1)}`)
|
|
32
|
+
);
|
|
33
|
+
}, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toPascalCase(input: string): string {
|
|
37
|
+
return splitWords(input)
|
|
38
|
+
.map((word) => `${word[0]!.toUpperCase()}${word.slice(1).toLowerCase()}`)
|
|
39
|
+
.join("");
|
|
40
|
+
}
|