@better-auth/core 1.4.17 → 1.4.19
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/.turbo/turbo-build.log +253 -176
- package/dist/api/index.d.mts +11 -5
- package/dist/api/index.mjs +2 -1
- package/dist/api/index.mjs.map +1 -0
- package/dist/async_hooks/index.d.mts +2 -1
- package/dist/async_hooks/index.mjs +2 -1
- package/dist/async_hooks/index.mjs.map +1 -0
- package/dist/async_hooks/pure.index.d.mts +2 -1
- package/dist/async_hooks/pure.index.mjs +2 -1
- package/dist/async_hooks/pure.index.mjs.map +1 -0
- package/dist/context/endpoint-context.d.mts +2 -1
- package/dist/context/endpoint-context.mjs +2 -1
- package/dist/context/endpoint-context.mjs.map +1 -0
- package/dist/context/global.d.mts +2 -1
- package/dist/context/global.mjs +3 -2
- package/dist/context/global.mjs.map +1 -0
- package/dist/context/request-state.d.mts +2 -1
- package/dist/context/request-state.mjs +2 -1
- package/dist/context/request-state.mjs.map +1 -0
- package/dist/context/transaction.d.mts +2 -1
- package/dist/context/transaction.mjs +2 -1
- package/dist/context/transaction.mjs.map +1 -0
- package/dist/db/adapter/factory.d.mts +2 -1
- package/dist/db/adapter/factory.mjs +9 -27
- package/dist/db/adapter/factory.mjs.map +1 -0
- package/dist/db/adapter/get-default-field-name.d.mts +2 -1
- package/dist/db/adapter/get-default-field-name.mjs +2 -1
- package/dist/db/adapter/get-default-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-default-model-name.d.mts +2 -1
- package/dist/db/adapter/get-default-model-name.mjs +2 -1
- package/dist/db/adapter/get-default-model-name.mjs.map +1 -0
- package/dist/db/adapter/get-field-attributes.d.mts +2 -1
- package/dist/db/adapter/get-field-attributes.mjs +2 -1
- package/dist/db/adapter/get-field-attributes.mjs.map +1 -0
- package/dist/db/adapter/get-field-name.d.mts +2 -1
- package/dist/db/adapter/get-field-name.mjs +2 -1
- package/dist/db/adapter/get-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-id-field.d.mts +2 -1
- package/dist/db/adapter/get-id-field.mjs +2 -1
- package/dist/db/adapter/get-id-field.mjs.map +1 -0
- package/dist/db/adapter/get-model-name.d.mts +2 -1
- package/dist/db/adapter/get-model-name.mjs +2 -1
- package/dist/db/adapter/get-model-name.mjs.map +1 -0
- package/dist/db/adapter/index.d.mts +5 -1
- package/dist/db/adapter/types.d.mts +2 -1
- package/dist/db/adapter/utils.d.mts +2 -1
- package/dist/db/adapter/utils.mjs +2 -1
- package/dist/db/adapter/utils.mjs.map +1 -0
- package/dist/db/get-tables.d.mts +2 -1
- package/dist/db/get-tables.mjs +8 -2
- package/dist/db/get-tables.mjs.map +1 -0
- package/dist/db/plugin.d.mts +2 -1
- package/dist/db/schema/account.d.mts +2 -1
- package/dist/db/schema/account.mjs +2 -1
- package/dist/db/schema/account.mjs.map +1 -0
- package/dist/db/schema/rate-limit.d.mts +2 -1
- package/dist/db/schema/rate-limit.mjs +2 -1
- package/dist/db/schema/rate-limit.mjs.map +1 -0
- package/dist/db/schema/session.d.mts +2 -1
- package/dist/db/schema/session.mjs +2 -1
- package/dist/db/schema/session.mjs.map +1 -0
- package/dist/db/schema/shared.d.mts +2 -1
- package/dist/db/schema/shared.mjs +2 -1
- package/dist/db/schema/shared.mjs.map +1 -0
- package/dist/db/schema/user.d.mts +2 -1
- package/dist/db/schema/user.mjs +2 -1
- package/dist/db/schema/user.mjs.map +1 -0
- package/dist/db/schema/verification.d.mts +2 -1
- package/dist/db/schema/verification.mjs +2 -1
- package/dist/db/schema/verification.mjs.map +1 -0
- package/dist/db/type.d.mts +2 -1
- package/dist/env/color-depth.d.mts +2 -1
- package/dist/env/color-depth.mjs +2 -1
- package/dist/env/color-depth.mjs.map +1 -0
- package/dist/env/env-impl.d.mts +3 -2
- package/dist/env/env-impl.mjs +3 -2
- package/dist/env/env-impl.mjs.map +1 -0
- package/dist/env/logger.d.mts +2 -1
- package/dist/env/logger.mjs +3 -2
- package/dist/env/logger.mjs.map +1 -0
- package/dist/error/codes.d.mts +2 -1
- package/dist/error/codes.mjs +2 -1
- package/dist/error/codes.mjs.map +1 -0
- package/dist/error/index.d.mts +2 -1
- package/dist/error/index.mjs +2 -1
- package/dist/error/index.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/oauth2/client-credentials-token.d.mts +2 -1
- package/dist/oauth2/client-credentials-token.mjs +2 -1
- package/dist/oauth2/client-credentials-token.mjs.map +1 -0
- package/dist/oauth2/create-authorization-url.d.mts +2 -1
- package/dist/oauth2/create-authorization-url.mjs +2 -1
- package/dist/oauth2/create-authorization-url.mjs.map +1 -0
- package/dist/oauth2/oauth-provider.d.mts +3 -2
- package/dist/oauth2/refresh-access-token.d.mts +2 -1
- package/dist/oauth2/refresh-access-token.mjs +2 -1
- package/dist/oauth2/refresh-access-token.mjs.map +1 -0
- package/dist/oauth2/utils.d.mts +2 -1
- package/dist/oauth2/utils.mjs +2 -1
- package/dist/oauth2/utils.mjs.map +1 -0
- package/dist/oauth2/validate-authorization-code.d.mts +6 -2
- package/dist/oauth2/validate-authorization-code.mjs +7 -12
- package/dist/oauth2/validate-authorization-code.mjs.map +1 -0
- package/dist/oauth2/verify.d.mts +2 -1
- package/dist/oauth2/verify.mjs +2 -1
- package/dist/oauth2/verify.mjs.map +1 -0
- package/dist/social-providers/apple.d.mts +2 -1
- package/dist/social-providers/apple.mjs +22 -15
- package/dist/social-providers/apple.mjs.map +1 -0
- package/dist/social-providers/atlassian.d.mts +2 -1
- package/dist/social-providers/atlassian.mjs +2 -1
- package/dist/social-providers/atlassian.mjs.map +1 -0
- package/dist/social-providers/cognito.d.mts +2 -1
- package/dist/social-providers/cognito.mjs +2 -1
- package/dist/social-providers/cognito.mjs.map +1 -0
- package/dist/social-providers/discord.d.mts +2 -1
- package/dist/social-providers/discord.mjs +2 -1
- package/dist/social-providers/discord.mjs.map +1 -0
- package/dist/social-providers/dropbox.d.mts +2 -1
- package/dist/social-providers/dropbox.mjs +2 -1
- package/dist/social-providers/dropbox.mjs.map +1 -0
- package/dist/social-providers/facebook.d.mts +2 -1
- package/dist/social-providers/facebook.mjs +5 -4
- package/dist/social-providers/facebook.mjs.map +1 -0
- package/dist/social-providers/figma.d.mts +2 -1
- package/dist/social-providers/figma.mjs +2 -1
- package/dist/social-providers/figma.mjs.map +1 -0
- package/dist/social-providers/github.d.mts +3 -2
- package/dist/social-providers/github.mjs +22 -5
- package/dist/social-providers/github.mjs.map +1 -0
- package/dist/social-providers/gitlab.d.mts +2 -1
- package/dist/social-providers/gitlab.mjs +2 -1
- package/dist/social-providers/gitlab.mjs.map +1 -0
- package/dist/social-providers/google.d.mts +2 -1
- package/dist/social-providers/google.mjs +18 -13
- package/dist/social-providers/google.mjs.map +1 -0
- package/dist/social-providers/huggingface.d.mts +2 -1
- package/dist/social-providers/huggingface.mjs +2 -1
- package/dist/social-providers/huggingface.mjs.map +1 -0
- package/dist/social-providers/index.d.mts +5 -3
- package/dist/social-providers/index.mjs +3 -2
- package/dist/social-providers/index.mjs.map +1 -0
- package/dist/social-providers/kakao.d.mts +2 -1
- package/dist/social-providers/kakao.mjs +2 -1
- package/dist/social-providers/kakao.mjs.map +1 -0
- package/dist/social-providers/kick.d.mts +2 -1
- package/dist/social-providers/kick.mjs +2 -1
- package/dist/social-providers/kick.mjs.map +1 -0
- package/dist/social-providers/line.d.mts +2 -1
- package/dist/social-providers/line.mjs +2 -1
- package/dist/social-providers/line.mjs.map +1 -0
- package/dist/social-providers/linear.d.mts +2 -1
- package/dist/social-providers/linear.mjs +2 -1
- package/dist/social-providers/linear.mjs.map +1 -0
- package/dist/social-providers/linkedin.d.mts +2 -1
- package/dist/social-providers/linkedin.mjs +2 -1
- package/dist/social-providers/linkedin.mjs.map +1 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +4 -1
- package/dist/social-providers/microsoft-entra-id.mjs +36 -2
- package/dist/social-providers/microsoft-entra-id.mjs.map +1 -0
- package/dist/social-providers/naver.d.mts +2 -1
- package/dist/social-providers/naver.mjs +2 -1
- package/dist/social-providers/naver.mjs.map +1 -0
- package/dist/social-providers/notion.d.mts +2 -1
- package/dist/social-providers/notion.mjs +2 -1
- package/dist/social-providers/notion.mjs.map +1 -0
- package/dist/social-providers/paybin.d.mts +2 -1
- package/dist/social-providers/paybin.mjs +2 -1
- package/dist/social-providers/paybin.mjs.map +1 -0
- package/dist/social-providers/paypal.d.mts +2 -1
- package/dist/social-providers/paypal.mjs +2 -1
- package/dist/social-providers/paypal.mjs.map +1 -0
- package/dist/social-providers/polar.d.mts +2 -1
- package/dist/social-providers/polar.mjs +2 -1
- package/dist/social-providers/polar.mjs.map +1 -0
- package/dist/social-providers/reddit.d.mts +2 -1
- package/dist/social-providers/reddit.mjs +2 -1
- package/dist/social-providers/reddit.mjs.map +1 -0
- package/dist/social-providers/roblox.d.mts +2 -1
- package/dist/social-providers/roblox.mjs +2 -1
- package/dist/social-providers/roblox.mjs.map +1 -0
- package/dist/social-providers/salesforce.d.mts +2 -1
- package/dist/social-providers/salesforce.mjs +2 -1
- package/dist/social-providers/salesforce.mjs.map +1 -0
- package/dist/social-providers/slack.d.mts +2 -1
- package/dist/social-providers/slack.mjs +2 -1
- package/dist/social-providers/slack.mjs.map +1 -0
- package/dist/social-providers/spotify.d.mts +2 -1
- package/dist/social-providers/spotify.mjs +2 -1
- package/dist/social-providers/spotify.mjs.map +1 -0
- package/dist/social-providers/tiktok.d.mts +2 -1
- package/dist/social-providers/tiktok.mjs +2 -1
- package/dist/social-providers/tiktok.mjs.map +1 -0
- package/dist/social-providers/twitch.d.mts +2 -1
- package/dist/social-providers/twitch.mjs +2 -1
- package/dist/social-providers/twitch.mjs.map +1 -0
- package/dist/social-providers/twitter.d.mts +3 -2
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/twitter.mjs.map +1 -0
- package/dist/social-providers/vercel.d.mts +2 -1
- package/dist/social-providers/vercel.mjs +2 -1
- package/dist/social-providers/vercel.mjs.map +1 -0
- package/dist/social-providers/vk.d.mts +2 -1
- package/dist/social-providers/vk.mjs +2 -1
- package/dist/social-providers/vk.mjs.map +1 -0
- package/dist/social-providers/zoom.d.mts +3 -3
- package/dist/social-providers/zoom.mjs +2 -1
- package/dist/social-providers/zoom.mjs.map +1 -0
- package/dist/types/context.d.mts +7 -3
- package/dist/types/cookie.d.mts +2 -1
- package/dist/types/helper.d.mts +2 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +36 -33
- package/dist/types/plugin-client.d.mts +2 -1
- package/dist/types/plugin.d.mts +2 -1
- package/dist/utils/db.d.mts +13 -0
- package/dist/utils/db.mjs +17 -0
- package/dist/utils/db.mjs.map +1 -0
- package/dist/utils/deprecate.d.mts +2 -1
- package/dist/utils/deprecate.mjs +2 -1
- package/dist/utils/deprecate.mjs.map +1 -0
- package/dist/utils/error-codes.d.mts +2 -1
- package/dist/utils/error-codes.mjs +2 -1
- package/dist/utils/error-codes.mjs.map +1 -0
- package/dist/utils/id.d.mts +2 -1
- package/dist/utils/id.mjs +2 -1
- package/dist/utils/id.mjs.map +1 -0
- package/dist/utils/index.d.mts +2 -1
- package/dist/utils/index.mjs +2 -1
- package/dist/utils/ip.d.mts +2 -1
- package/dist/utils/ip.mjs +2 -1
- package/dist/utils/ip.mjs.map +1 -0
- package/dist/utils/json.d.mts +2 -1
- package/dist/utils/json.mjs +2 -1
- package/dist/utils/json.mjs.map +1 -0
- package/dist/utils/string.d.mts +2 -1
- package/dist/utils/string.mjs +2 -1
- package/dist/utils/string.mjs.map +1 -0
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +2 -1
- package/dist/utils/url.mjs.map +1 -0
- package/package.json +1 -1
- package/src/db/adapter/factory.ts +5 -41
- package/src/db/adapter/index.ts +3 -0
- package/src/db/get-tables.ts +5 -0
- package/src/env/env-impl.ts +2 -2
- package/src/env/logger.ts +1 -1
- package/src/oauth2/oauth-provider.ts +1 -1
- package/src/oauth2/validate-authorization-code.ts +13 -26
- package/src/oauth2/validate-token.test.ts +241 -0
- package/src/social-providers/apple.ts +37 -24
- package/src/social-providers/facebook.ts +3 -3
- package/src/social-providers/github.ts +25 -3
- package/src/social-providers/google.ts +18 -14
- package/src/social-providers/microsoft-entra-id.ts +84 -1
- package/src/types/context.ts +6 -3
- package/src/types/index.ts +6 -1
- package/src/types/init-options.ts +44 -36
- package/src/utils/db.ts +20 -0
- package/src/utils/index.ts +1 -0
- package/tsdown.config.ts +3 -0
|
@@ -6,4 +6,5 @@ type InvalidKeyError<K$1 extends string> = `Invalid error code key: "${K$1}" - m
|
|
|
6
6
|
type ValidateErrorCodes<T> = { [K in keyof T]: K extends string ? IsValidUpperSnakeCase<K> extends false ? InvalidKeyError<K> : T[K] : T[K] };
|
|
7
7
|
declare function defineErrorCodes<const T extends Record<string, string>>(codes: ValidateErrorCodes<T>): T;
|
|
8
8
|
//#endregion
|
|
9
|
-
export { defineErrorCodes };
|
|
9
|
+
export { defineErrorCodes };
|
|
10
|
+
//# sourceMappingURL=error-codes.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes.mjs","names":[],"sources":["../../src/utils/error-codes.ts"],"sourcesContent":["type UpperLetter =\n\t| \"A\"\n\t| \"B\"\n\t| \"C\"\n\t| \"D\"\n\t| \"E\"\n\t| \"F\"\n\t| \"G\"\n\t| \"H\"\n\t| \"I\"\n\t| \"J\"\n\t| \"K\"\n\t| \"L\"\n\t| \"M\"\n\t| \"N\"\n\t| \"O\"\n\t| \"P\"\n\t| \"Q\"\n\t| \"R\"\n\t| \"S\"\n\t| \"T\"\n\t| \"U\"\n\t| \"V\"\n\t| \"W\"\n\t| \"X\"\n\t| \"Y\"\n\t| \"Z\";\ntype SpecialCharacter = \"_\";\n\ntype IsValidUpperSnakeCase<S extends string> = S extends `${infer F}${infer R}`\n\t? F extends UpperLetter | SpecialCharacter\n\t\t? IsValidUpperSnakeCase<R>\n\t\t: false\n\t: true;\n\ntype InvalidKeyError<K extends string> =\n\t`Invalid error code key: \"${K}\" - must only contain uppercase letters (A-Z) and underscores (_)`;\n\ntype ValidateErrorCodes<T> = {\n\t[K in keyof T]: K extends string\n\t\t? IsValidUpperSnakeCase<K> extends false\n\t\t\t? InvalidKeyError<K>\n\t\t\t: T[K]\n\t\t: T[K];\n};\n\nexport function defineErrorCodes<const T extends Record<string, string>>(\n\tcodes: ValidateErrorCodes<T>,\n): T {\n\treturn codes as T;\n}\n"],"mappings":";AA8CA,SAAgB,iBACf,OACI;AACJ,QAAO"}
|
package/dist/utils/id.d.mts
CHANGED
package/dist/utils/id.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.mjs","names":[],"sources":["../../src/utils/id.ts"],"sourcesContent":["import { createRandomStringGenerator } from \"@better-auth/utils/random\";\n\nexport const generateId = (size?: number) => {\n\treturn createRandomStringGenerator(\"a-z\", \"A-Z\", \"0-9\")(size || 32);\n};\n"],"mappings":";;;AAEA,MAAa,cAAc,SAAkB;AAC5C,QAAO,4BAA4B,OAAO,OAAO,MAAM,CAAC,QAAQ,GAAG"}
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { filterOutputFields } from "./db.mjs";
|
|
1
2
|
import { deprecate } from "./deprecate.mjs";
|
|
2
3
|
import { defineErrorCodes } from "./error-codes.mjs";
|
|
3
4
|
import { generateId } from "./id.mjs";
|
|
@@ -5,4 +6,4 @@ import { createRateLimitKey, isValidIP, normalizeIP } from "./ip.mjs";
|
|
|
5
6
|
import { safeJSONParse } from "./json.mjs";
|
|
6
7
|
import { capitalizeFirstLetter } from "./string.mjs";
|
|
7
8
|
import { normalizePathname } from "./url.mjs";
|
|
8
|
-
export { capitalizeFirstLetter, createRateLimitKey, defineErrorCodes, deprecate, generateId, isValidIP, normalizeIP, normalizePathname, safeJSONParse };
|
|
9
|
+
export { capitalizeFirstLetter, createRateLimitKey, defineErrorCodes, deprecate, filterOutputFields, generateId, isValidIP, normalizeIP, normalizePathname, safeJSONParse };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { filterOutputFields } from "./db.mjs";
|
|
1
2
|
import { deprecate } from "./deprecate.mjs";
|
|
2
3
|
import { defineErrorCodes } from "./error-codes.mjs";
|
|
3
4
|
import { generateId } from "./id.mjs";
|
|
@@ -6,4 +7,4 @@ import { safeJSONParse } from "./json.mjs";
|
|
|
6
7
|
import { capitalizeFirstLetter } from "./string.mjs";
|
|
7
8
|
import { normalizePathname } from "./url.mjs";
|
|
8
9
|
|
|
9
|
-
export { capitalizeFirstLetter, createRateLimitKey, defineErrorCodes, deprecate, generateId, isValidIP, normalizeIP, normalizePathname, safeJSONParse };
|
|
10
|
+
export { capitalizeFirstLetter, createRateLimitKey, defineErrorCodes, deprecate, filterOutputFields, generateId, isValidIP, normalizeIP, normalizePathname, safeJSONParse };
|
package/dist/utils/ip.d.mts
CHANGED
|
@@ -51,4 +51,5 @@ declare function normalizeIP(ip: string, options?: NormalizeIPOptions): string;
|
|
|
51
51
|
*/
|
|
52
52
|
declare function createRateLimitKey(ip: string, path: string): string;
|
|
53
53
|
//#endregion
|
|
54
|
-
export { createRateLimitKey, isValidIP, normalizeIP };
|
|
54
|
+
export { createRateLimitKey, isValidIP, normalizeIP };
|
|
55
|
+
//# sourceMappingURL=ip.d.mts.map
|
package/dist/utils/ip.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip.mjs","names":["bitsRemaining: number"],"sources":["../../src/utils/ip.ts"],"sourcesContent":["import * as z from \"zod\";\n\n/**\n * Normalizes an IP address for consistent rate limiting.\n *\n * Features:\n * - Normalizes IPv6 to canonical lowercase form\n * - Converts IPv4-mapped IPv6 to IPv4\n * - Supports IPv6 subnet extraction\n * - Handles all edge cases (::1, ::, etc.)\n */\n\ninterface NormalizeIPOptions {\n\t/**\n\t * For IPv6 addresses, extract the subnet prefix instead of full address.\n\t * Common values: 32, 48, 64, 128 (default: 128 = full address)\n\t *\n\t * @default 128\n\t */\n\tipv6Subnet?: 128 | 64 | 48 | 32;\n}\n\n/**\n * Checks if an IP is valid IPv4 or IPv6\n */\nexport function isValidIP(ip: string): boolean {\n\treturn z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;\n}\n\n/**\n * Checks if an IP is IPv6\n */\nfunction isIPv6(ip: string): boolean {\n\treturn z.ipv6().safeParse(ip).success;\n}\n\n/**\n * Converts IPv4-mapped IPv6 address to IPv4\n * e.g., \"::ffff:192.0.2.1\" -> \"192.0.2.1\"\n */\nfunction extractIPv4FromMapped(ipv6: string): string | null {\n\tconst lower = ipv6.toLowerCase();\n\n\t// Handle ::ffff:192.0.2.1 format\n\tif (lower.startsWith(\"::ffff:\")) {\n\t\tconst ipv4Part = lower.substring(7);\n\t\t// Check if it's a valid IPv4\n\t\tif (z.ipv4().safeParse(ipv4Part).success) {\n\t\t\treturn ipv4Part;\n\t\t}\n\t}\n\n\t// Handle full form: 0:0:0:0:0:ffff:192.0.2.1\n\tconst parts = ipv6.split(\":\");\n\tif (parts.length === 7 && parts[5]?.toLowerCase() === \"ffff\") {\n\t\tconst ipv4Part = parts[6];\n\t\tif (ipv4Part && z.ipv4().safeParse(ipv4Part).success) {\n\t\t\treturn ipv4Part;\n\t\t}\n\t}\n\n\t// Handle hex-encoded IPv4 in mapped address\n\t// e.g., ::ffff:c000:0201 -> 192.0.2.1\n\tif (lower.includes(\"::ffff:\") || lower.includes(\":ffff:\")) {\n\t\tconst groups = expandIPv6(ipv6);\n\t\tif (\n\t\t\tgroups.length === 8 &&\n\t\t\tgroups[0] === \"0000\" &&\n\t\t\tgroups[1] === \"0000\" &&\n\t\t\tgroups[2] === \"0000\" &&\n\t\t\tgroups[3] === \"0000\" &&\n\t\t\tgroups[4] === \"0000\" &&\n\t\t\tgroups[5] === \"ffff\" &&\n\t\t\tgroups[6] &&\n\t\t\tgroups[7]\n\t\t) {\n\t\t\t// Convert last two groups to IPv4\n\t\t\tconst byte1 = Number.parseInt(groups[6].substring(0, 2), 16);\n\t\t\tconst byte2 = Number.parseInt(groups[6].substring(2, 4), 16);\n\t\t\tconst byte3 = Number.parseInt(groups[7].substring(0, 2), 16);\n\t\t\tconst byte4 = Number.parseInt(groups[7].substring(2, 4), 16);\n\t\t\treturn `${byte1}.${byte2}.${byte3}.${byte4}`;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Expands a compressed IPv6 address to full form\n * e.g., \"2001:db8::1\" -> [\"2001\", \"0db8\", \"0000\", \"0000\", \"0000\", \"0000\", \"0000\", \"0001\"]\n */\nfunction expandIPv6(ipv6: string): string[] {\n\t// Handle :: notation (zero compression)\n\tif (ipv6.includes(\"::\")) {\n\t\tconst sides = ipv6.split(\"::\");\n\t\tconst left = sides[0] ? sides[0].split(\":\") : [];\n\t\tconst right = sides[1] ? sides[1].split(\":\") : [];\n\n\t\t// Calculate missing groups\n\t\tconst totalGroups = 8;\n\t\tconst missingGroups = totalGroups - left.length - right.length;\n\t\tconst zeros = Array(missingGroups).fill(\"0000\");\n\n\t\t// Pad existing groups to 4 digits\n\t\tconst paddedLeft = left.map((g) => g.padStart(4, \"0\"));\n\t\tconst paddedRight = right.map((g) => g.padStart(4, \"0\"));\n\n\t\treturn [...paddedLeft, ...zeros, ...paddedRight];\n\t}\n\n\t// No compression, just pad each group\n\treturn ipv6.split(\":\").map((g) => g.padStart(4, \"0\"));\n}\n\n/**\n * Normalizes an IPv6 address to canonical form\n * e.g., \"2001:DB8::1\" -> \"2001:0db8:0000:0000:0000:0000:0000:0001\"\n */\nfunction normalizeIPv6(\n\tipv6: string,\n\tsubnetPrefix?: 128 | 32 | 48 | 64,\n): string {\n\tconst groups = expandIPv6(ipv6);\n\n\tif (subnetPrefix && subnetPrefix < 128) {\n\t\t// Apply subnet mask\n\t\tconst prefix = subnetPrefix;\n\t\tlet bitsRemaining: number = prefix;\n\n\t\tconst maskedGroups = groups.map((group) => {\n\t\t\tif (bitsRemaining <= 0) {\n\t\t\t\treturn \"0000\";\n\t\t\t}\n\t\t\tif (bitsRemaining >= 16) {\n\t\t\t\tbitsRemaining -= 16;\n\t\t\t\treturn group;\n\t\t\t}\n\n\t\t\t// Partial mask for this group\n\t\t\tconst value = Number.parseInt(group, 16);\n\t\t\tconst mask = (0xffff << (16 - bitsRemaining)) & 0xffff;\n\t\t\tconst masked = value & mask;\n\t\t\tbitsRemaining = 0;\n\t\t\treturn masked.toString(16).padStart(4, \"0\");\n\t\t});\n\n\t\treturn maskedGroups.join(\":\").toLowerCase();\n\t}\n\n\treturn groups.join(\":\").toLowerCase();\n}\n\n/**\n * Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.\n *\n * @param ip - The IP address to normalize\n * @param options - Normalization options\n * @returns Normalized IP address\n *\n * @example\n * normalizeIP(\"2001:DB8::1\")\n * // -> \"2001:0db8:0000:0000:0000:0000:0000:0000\"\n *\n * @example\n * normalizeIP(\"::ffff:192.0.2.1\")\n * // -> \"192.0.2.1\" (converted to IPv4)\n *\n * @example\n * normalizeIP(\"2001:db8::1\", { ipv6Subnet: 64 })\n * // -> \"2001:0db8:0000:0000:0000:0000:0000:0000\" (subnet /64)\n */\nexport function normalizeIP(\n\tip: string,\n\toptions: NormalizeIPOptions = {},\n): string {\n\t// IPv4 addresses are already normalized\n\tif (z.ipv4().safeParse(ip).success) {\n\t\treturn ip.toLowerCase();\n\t}\n\n\t// Check if it's IPv6\n\tif (!isIPv6(ip)) {\n\t\t// Return as-is if not valid (shouldn't happen due to prior validation)\n\t\treturn ip.toLowerCase();\n\t}\n\n\t// Check for IPv4-mapped IPv6\n\tconst ipv4 = extractIPv4FromMapped(ip);\n\tif (ipv4) {\n\t\treturn ipv4.toLowerCase();\n\t}\n\n\t// Normalize IPv6\n\tconst subnetPrefix = options.ipv6Subnet || 64;\n\treturn normalizeIPv6(ip, subnetPrefix);\n}\n\n/**\n * Creates a rate limit key from IP and path\n * Uses a separator to prevent collision attacks\n *\n * @param ip - The IP address (should be normalized)\n * @param path - The request path\n * @returns Rate limit key\n */\nexport function createRateLimitKey(ip: string, path: string): string {\n\t// Use | as separator to prevent collision attacks\n\t// e.g., \"192.0.2.1\" + \"/sign-in\" vs \"192.0.2\" + \".1/sign-in\"\n\treturn `${ip}|${path}`;\n}\n"],"mappings":"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"}
|
package/dist/utils/json.d.mts
CHANGED
package/dist/utils/json.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.mjs","names":[],"sources":["../../src/utils/json.ts"],"sourcesContent":["import { logger } from \"../env\";\n\nexport function safeJSONParse<T>(data: unknown): T | null {\n\tfunction reviver(_: string, value: any): any {\n\t\tif (typeof value === \"string\") {\n\t\t\tconst iso8601Regex = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$/;\n\t\t\tif (iso8601Regex.test(value)) {\n\t\t\t\tconst date = new Date(value);\n\t\t\t\tif (!isNaN(date.getTime())) {\n\t\t\t\t\treturn date;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\ttry {\n\t\tif (typeof data !== \"string\") {\n\t\t\treturn data as T;\n\t\t}\n\t\treturn JSON.parse(data, reviver);\n\t} catch (e) {\n\t\tlogger.error(\"Error parsing JSON\", { error: e });\n\t\treturn null;\n\t}\n}\n"],"mappings":";;;;AAEA,SAAgB,cAAiB,MAAyB;CACzD,SAAS,QAAQ,GAAW,OAAiB;AAC5C,MAAI,OAAO,UAAU,UAEpB;OADqB,mDACJ,KAAK,MAAM,EAAE;IAC7B,MAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,QAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CACzB,QAAO;;;AAIV,SAAO;;AAER,KAAI;AACH,MAAI,OAAO,SAAS,SACnB,QAAO;AAER,SAAO,KAAK,MAAM,MAAM,QAAQ;UACxB,GAAG;AACX,SAAO,MAAM,sBAAsB,EAAE,OAAO,GAAG,CAAC;AAChD,SAAO"}
|
package/dist/utils/string.d.mts
CHANGED
package/dist/utils/string.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string.mjs","names":[],"sources":["../../src/utils/string.ts"],"sourcesContent":["export function capitalizeFirstLetter(str: string) {\n\treturn str.charAt(0).toUpperCase() + str.slice(1);\n}\n"],"mappings":";AAAA,SAAgB,sBAAsB,KAAa;AAClD,QAAO,IAAI,OAAO,EAAE,CAAC,aAAa,GAAG,IAAI,MAAM,EAAE"}
|
package/dist/utils/url.d.mts
CHANGED
package/dist/utils/url.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.mjs","names":["pathname: string"],"sources":["../../src/utils/url.ts"],"sourcesContent":["/**\n * Normalizes a request pathname by removing the basePath prefix and trailing slashes.\n * This is useful for matching paths against configured path lists.\n *\n * @param requestUrl - The full request URL\n * @param basePath - The base path of the auth API (e.g., \"/api/auth\")\n * @returns The normalized path without basePath prefix or trailing slashes,\n * or \"/\" if URL parsing fails\n *\n * @example\n * normalizePathname(\"http://localhost:3000/api/auth/sso/saml2/callback/provider1\", \"/api/auth\")\n * // Returns: \"/sso/saml2/callback/provider1\"\n *\n * normalizePathname(\"http://localhost:3000/sso/saml2/callback/provider1/\", \"/\")\n * // Returns: \"/sso/saml2/callback/provider1\"\n */\nexport function normalizePathname(\n\trequestUrl: string,\n\tbasePath: string,\n): string {\n\tlet pathname: string;\n\ttry {\n\t\tpathname = new URL(requestUrl).pathname.replace(/\\/+$/, \"\") || \"/\";\n\t} catch {\n\t\treturn \"/\";\n\t}\n\n\tif (basePath === \"/\" || basePath === \"\") {\n\t\treturn pathname;\n\t}\n\n\t// Check for exact match or proper path boundary (basePath followed by \"/\" or end)\n\t// This prevents \"/api/auth\" from matching \"/api/authevil/...\"\n\tif (pathname === basePath) {\n\t\treturn \"/\";\n\t}\n\n\tif (pathname.startsWith(basePath + \"/\")) {\n\t\treturn pathname.slice(basePath.length).replace(/\\/+$/, \"\") || \"/\";\n\t}\n\n\treturn pathname;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,kBACf,YACA,UACS;CACT,IAAIA;AACJ,KAAI;AACH,aAAW,IAAI,IAAI,WAAW,CAAC,SAAS,QAAQ,QAAQ,GAAG,IAAI;SACxD;AACP,SAAO;;AAGR,KAAI,aAAa,OAAO,aAAa,GACpC,QAAO;AAKR,KAAI,aAAa,SAChB,QAAO;AAGR,KAAI,SAAS,WAAW,WAAW,IAAI,CACtC,QAAO,SAAS,MAAM,SAAS,OAAO,CAAC,QAAQ,QAAQ,GAAG,IAAI;AAG/D,QAAO"}
|
package/package.json
CHANGED
|
@@ -1083,6 +1083,7 @@ export const createAdapterFactory =
|
|
|
1083
1083
|
model: unsafeModel,
|
|
1084
1084
|
where: unsafeWhere,
|
|
1085
1085
|
limit: unsafeLimit,
|
|
1086
|
+
select,
|
|
1086
1087
|
sortBy,
|
|
1087
1088
|
offset,
|
|
1088
1089
|
join: unsafeJoin,
|
|
@@ -1090,6 +1091,7 @@ export const createAdapterFactory =
|
|
|
1090
1091
|
model: string;
|
|
1091
1092
|
where?: Where[];
|
|
1092
1093
|
limit?: number;
|
|
1094
|
+
select?: string[] | undefined;
|
|
1093
1095
|
sortBy?: { field: string; direction: "asc" | "desc" };
|
|
1094
1096
|
offset?: number;
|
|
1095
1097
|
join?: JoinOption;
|
|
@@ -1110,13 +1112,10 @@ export const createAdapterFactory =
|
|
|
1110
1112
|
let join: JoinConfig | undefined;
|
|
1111
1113
|
let passJoinToAdapter = true;
|
|
1112
1114
|
if (!config.disableTransformJoin) {
|
|
1113
|
-
const result = transformJoinClause(
|
|
1114
|
-
unsafeModel,
|
|
1115
|
-
unsafeJoin,
|
|
1116
|
-
undefined,
|
|
1117
|
-
);
|
|
1115
|
+
const result = transformJoinClause(unsafeModel, unsafeJoin, select);
|
|
1118
1116
|
if (result) {
|
|
1119
1117
|
join = result.join;
|
|
1118
|
+
select = result.select;
|
|
1120
1119
|
}
|
|
1121
1120
|
// If adapter doesn't support joins and we have joins, don't pass them to the adapter
|
|
1122
1121
|
const experimentalJoins = options.experimental?.joins;
|
|
@@ -1137,6 +1136,7 @@ export const createAdapterFactory =
|
|
|
1137
1136
|
model,
|
|
1138
1137
|
where,
|
|
1139
1138
|
limit: limit,
|
|
1139
|
+
select,
|
|
1140
1140
|
sortBy,
|
|
1141
1141
|
offset,
|
|
1142
1142
|
join: passJoinToAdapter ? join : undefined,
|
|
@@ -1284,42 +1284,6 @@ export const createAdapterFactory =
|
|
|
1284
1284
|
delete tables.session;
|
|
1285
1285
|
}
|
|
1286
1286
|
|
|
1287
|
-
if (
|
|
1288
|
-
options.rateLimit &&
|
|
1289
|
-
options.rateLimit.storage === "database" &&
|
|
1290
|
-
// rate-limit will default to enabled in production,
|
|
1291
|
-
// and given storage is database, it will try to use the rate-limit table,
|
|
1292
|
-
// so we should make sure to generate rate-limit table schema
|
|
1293
|
-
(typeof options.rateLimit.enabled === "undefined" ||
|
|
1294
|
-
// and of course if they forcefully set to true, then they want rate-limit,
|
|
1295
|
-
// thus we should also generate rate-limit table schema
|
|
1296
|
-
options.rateLimit.enabled === true)
|
|
1297
|
-
) {
|
|
1298
|
-
tables.ratelimit = {
|
|
1299
|
-
modelName: options.rateLimit.modelName ?? "ratelimit",
|
|
1300
|
-
fields: {
|
|
1301
|
-
key: {
|
|
1302
|
-
type: "string",
|
|
1303
|
-
unique: true,
|
|
1304
|
-
required: true,
|
|
1305
|
-
fieldName: options.rateLimit.fields?.key ?? "key",
|
|
1306
|
-
},
|
|
1307
|
-
count: {
|
|
1308
|
-
type: "number",
|
|
1309
|
-
required: true,
|
|
1310
|
-
fieldName: options.rateLimit.fields?.count ?? "count",
|
|
1311
|
-
},
|
|
1312
|
-
lastRequest: {
|
|
1313
|
-
type: "number",
|
|
1314
|
-
required: true,
|
|
1315
|
-
bigint: true,
|
|
1316
|
-
defaultValue: () => Date.now(),
|
|
1317
|
-
fieldName:
|
|
1318
|
-
options.rateLimit.fields?.lastRequest ?? "lastRequest",
|
|
1319
|
-
},
|
|
1320
|
-
},
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
1287
|
return adapterInstance.createSchema!({ file, tables });
|
|
1324
1288
|
}
|
|
1325
1289
|
: undefined,
|
package/src/db/adapter/index.ts
CHANGED
|
@@ -405,6 +405,7 @@ export type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
|
|
|
405
405
|
model: string;
|
|
406
406
|
where?: Where[] | undefined;
|
|
407
407
|
limit?: number | undefined;
|
|
408
|
+
select?: string[] | undefined;
|
|
408
409
|
sortBy?:
|
|
409
410
|
| {
|
|
410
411
|
field: string;
|
|
@@ -493,6 +494,7 @@ export interface CustomAdapter {
|
|
|
493
494
|
model,
|
|
494
495
|
where,
|
|
495
496
|
limit,
|
|
497
|
+
select,
|
|
496
498
|
sortBy,
|
|
497
499
|
offset,
|
|
498
500
|
join,
|
|
@@ -500,6 +502,7 @@ export interface CustomAdapter {
|
|
|
500
502
|
model: string;
|
|
501
503
|
where?: CleanedWhere[] | undefined;
|
|
502
504
|
limit: number;
|
|
505
|
+
select?: string[] | undefined;
|
|
503
506
|
sortBy?: { field: string; direction: "asc" | "desc" } | undefined;
|
|
504
507
|
offset?: number | undefined;
|
|
505
508
|
join?: JoinConfig | undefined;
|
package/src/db/get-tables.ts
CHANGED
|
@@ -32,16 +32,21 @@ export const getAuthTables = (
|
|
|
32
32
|
fields: {
|
|
33
33
|
key: {
|
|
34
34
|
type: "string",
|
|
35
|
+
unique: true,
|
|
36
|
+
required: true,
|
|
35
37
|
fieldName: options.rateLimit?.fields?.key || "key",
|
|
36
38
|
},
|
|
37
39
|
count: {
|
|
38
40
|
type: "number",
|
|
41
|
+
required: true,
|
|
39
42
|
fieldName: options.rateLimit?.fields?.count || "count",
|
|
40
43
|
},
|
|
41
44
|
lastRequest: {
|
|
42
45
|
type: "number",
|
|
43
46
|
bigint: true,
|
|
47
|
+
required: true,
|
|
44
48
|
fieldName: options.rateLimit?.fields?.lastRequest || "lastRequest",
|
|
49
|
+
defaultValue: () => Date.now(),
|
|
45
50
|
},
|
|
46
51
|
},
|
|
47
52
|
},
|
package/src/env/env-impl.ts
CHANGED
|
@@ -115,10 +115,10 @@ export const ENV = Object.freeze({
|
|
|
115
115
|
get PACKAGE_VERSION() {
|
|
116
116
|
return getEnvVar("PACKAGE_VERSION", "0.0.0");
|
|
117
117
|
},
|
|
118
|
-
get BETTER_AUTH_TELEMETRY_ENDPOINT() {
|
|
118
|
+
get BETTER_AUTH_TELEMETRY_ENDPOINT(): string | undefined {
|
|
119
119
|
return getEnvVar(
|
|
120
120
|
"BETTER_AUTH_TELEMETRY_ENDPOINT",
|
|
121
|
-
|
|
121
|
+
import.meta.env.BETTER_AUTH_TELEMETRY_ENDPOINT,
|
|
122
122
|
);
|
|
123
123
|
},
|
|
124
124
|
});
|
package/src/env/logger.ts
CHANGED
|
@@ -94,7 +94,7 @@ export type InternalLogger = {
|
|
|
94
94
|
|
|
95
95
|
export const createLogger = (options?: Logger | undefined): InternalLogger => {
|
|
96
96
|
const enabled = options?.disabled !== true;
|
|
97
|
-
const logLevel = options?.level ?? "
|
|
97
|
+
const logLevel = options?.level ?? "warn";
|
|
98
98
|
|
|
99
99
|
const isDisableColorsSpecified = options?.disableColors !== undefined;
|
|
100
100
|
const colorsEnabled = isDisableColorsSpecified
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { base64 } from "@better-auth/utils/base64";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import { jwtVerify } from "jose";
|
|
3
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
4
4
|
import type { ProviderOptions } from "./index";
|
|
5
5
|
import { getOAuth2Tokens } from "./index";
|
|
6
6
|
|
|
@@ -124,31 +124,18 @@ export async function validateAuthorizationCode({
|
|
|
124
124
|
return tokens;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export async function validateToken(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
headers: {
|
|
140
|
-
accept: "application/json",
|
|
141
|
-
},
|
|
127
|
+
export async function validateToken(
|
|
128
|
+
token: string,
|
|
129
|
+
jwksEndpoint: string,
|
|
130
|
+
options?: {
|
|
131
|
+
audience?: string | string[];
|
|
132
|
+
issuer?: string | string[];
|
|
133
|
+
},
|
|
134
|
+
) {
|
|
135
|
+
const jwks = createRemoteJWKSet(new URL(jwksEndpoint));
|
|
136
|
+
const verified = await jwtVerify(token, jwks, {
|
|
137
|
+
audience: options?.audience,
|
|
138
|
+
issuer: options?.issuer,
|
|
142
139
|
});
|
|
143
|
-
if (error) {
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
const keys = data["keys"];
|
|
147
|
-
const header = JSON.parse(atob(token.split(".")[0]!));
|
|
148
|
-
const key = keys.find((key) => key.kid === header.kid);
|
|
149
|
-
if (!key) {
|
|
150
|
-
throw new Error("Key not found");
|
|
151
|
-
}
|
|
152
|
-
const verified = await jwtVerify(token, key);
|
|
153
140
|
return verified;
|
|
154
141
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { JWK } from "jose";
|
|
2
|
+
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
|
3
|
+
import {
|
|
4
|
+
afterAll,
|
|
5
|
+
beforeAll,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
it,
|
|
10
|
+
vi,
|
|
11
|
+
} from "vitest";
|
|
12
|
+
import { validateToken } from "./validate-authorization-code";
|
|
13
|
+
|
|
14
|
+
describe("validateToken", () => {
|
|
15
|
+
const originalFetch = globalThis.fetch;
|
|
16
|
+
const mockedFetch = vi.fn() as unknown as typeof fetch &
|
|
17
|
+
ReturnType<typeof vi.fn>;
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
globalThis.fetch = mockedFetch;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
globalThis.fetch = originalFetch;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mockedFetch.mockReset();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function createTestJWKS(alg: string, crv?: string) {
|
|
32
|
+
const { publicKey, privateKey } = await generateKeyPair(alg, {
|
|
33
|
+
crv,
|
|
34
|
+
extractable: true,
|
|
35
|
+
});
|
|
36
|
+
const publicJWK = await exportJWK(publicKey);
|
|
37
|
+
const privateJWK = await exportJWK(privateKey);
|
|
38
|
+
const kid = `test-key-${Date.now()}`;
|
|
39
|
+
publicJWK.kid = kid;
|
|
40
|
+
publicJWK.alg = alg;
|
|
41
|
+
privateJWK.kid = kid;
|
|
42
|
+
privateJWK.alg = alg;
|
|
43
|
+
return { publicJWK, privateJWK, kid, publicKey, privateKey };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function createSignedToken(
|
|
47
|
+
privateKey: CryptoKey,
|
|
48
|
+
alg: string,
|
|
49
|
+
kid: string,
|
|
50
|
+
payload: Record<string, unknown> = {},
|
|
51
|
+
) {
|
|
52
|
+
return await new SignJWT({
|
|
53
|
+
sub: "user-123",
|
|
54
|
+
email: "test@example.com",
|
|
55
|
+
iss: "https://example.com",
|
|
56
|
+
aud: "test-client",
|
|
57
|
+
...payload,
|
|
58
|
+
})
|
|
59
|
+
.setProtectedHeader({ alg, kid })
|
|
60
|
+
.setIssuedAt()
|
|
61
|
+
.setExpirationTime("1h")
|
|
62
|
+
.sign(privateKey);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mockJWKSResponse(...publicJWKs: JWK[]) {
|
|
66
|
+
mockedFetch.mockResolvedValueOnce(
|
|
67
|
+
new Response(JSON.stringify({ keys: publicJWKs }), {
|
|
68
|
+
status: 200,
|
|
69
|
+
headers: { "content-type": "application/json" },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
it("should verify RS256 signed token", async () => {
|
|
75
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
76
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
77
|
+
mockJWKSResponse(publicJWK);
|
|
78
|
+
|
|
79
|
+
const result = await validateToken(
|
|
80
|
+
token,
|
|
81
|
+
"https://example.com/.well-known/jwks",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(result).toBeDefined();
|
|
85
|
+
expect(result.payload.sub).toBe("user-123");
|
|
86
|
+
expect(result.payload.email).toBe("test@example.com");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should verify ES256 signed token", async () => {
|
|
90
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("ES256");
|
|
91
|
+
const token = await createSignedToken(privateKey, "ES256", kid);
|
|
92
|
+
mockJWKSResponse(publicJWK);
|
|
93
|
+
|
|
94
|
+
const result = await validateToken(
|
|
95
|
+
token,
|
|
96
|
+
"https://example.com/.well-known/jwks",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(result).toBeDefined();
|
|
100
|
+
expect(result.payload.sub).toBe("user-123");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should verify EdDSA (Ed25519) signed token", async () => {
|
|
104
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS(
|
|
105
|
+
"EdDSA",
|
|
106
|
+
"Ed25519",
|
|
107
|
+
);
|
|
108
|
+
const token = await createSignedToken(privateKey, "EdDSA", kid);
|
|
109
|
+
mockJWKSResponse(publicJWK);
|
|
110
|
+
|
|
111
|
+
const result = await validateToken(
|
|
112
|
+
token,
|
|
113
|
+
"https://example.com/.well-known/jwks",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result).toBeDefined();
|
|
117
|
+
expect(result.payload.sub).toBe("user-123");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should throw when kid doesn't match any key", async () => {
|
|
121
|
+
const { publicJWK, privateKey } = await createTestJWKS("RS256");
|
|
122
|
+
publicJWK.kid = "different-kid";
|
|
123
|
+
const token = await createSignedToken(privateKey, "RS256", "original-kid");
|
|
124
|
+
mockJWKSResponse(publicJWK);
|
|
125
|
+
|
|
126
|
+
await expect(
|
|
127
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
128
|
+
).rejects.toThrow();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should find correct key when multiple keys exist", async () => {
|
|
132
|
+
const key1 = await createTestJWKS("RS256");
|
|
133
|
+
const key2 = await createTestJWKS("RS256");
|
|
134
|
+
const key3 = await createTestJWKS("ES256");
|
|
135
|
+
const token = await createSignedToken(key2.privateKey, "RS256", key2.kid);
|
|
136
|
+
mockJWKSResponse(key1.publicJWK, key2.publicJWK, key3.publicJWK);
|
|
137
|
+
|
|
138
|
+
const result = await validateToken(
|
|
139
|
+
token,
|
|
140
|
+
"https://example.com/.well-known/jwks",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result).toBeDefined();
|
|
144
|
+
expect(result.payload.sub).toBe("user-123");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should throw when JWKS returns empty keys array", async () => {
|
|
148
|
+
const { privateKey, kid } = await createTestJWKS("RS256");
|
|
149
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
150
|
+
mockJWKSResponse();
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
154
|
+
).rejects.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should throw when JWKS fetch fails", async () => {
|
|
158
|
+
const { privateKey, kid } = await createTestJWKS("RS256");
|
|
159
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
160
|
+
mockedFetch.mockResolvedValueOnce(
|
|
161
|
+
new Response("Internal Server Error", { status: 500 }),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
validateToken(token, "https://example.com/.well-known/jwks"),
|
|
166
|
+
).rejects.toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should verify token with matching audience", async () => {
|
|
170
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
171
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
172
|
+
mockJWKSResponse(publicJWK);
|
|
173
|
+
|
|
174
|
+
const result = await validateToken(
|
|
175
|
+
token,
|
|
176
|
+
"https://example.com/.well-known/jwks",
|
|
177
|
+
{ audience: "test-client" },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(result).toBeDefined();
|
|
181
|
+
expect(result.payload.aud).toBe("test-client");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should reject token with mismatched audience", async () => {
|
|
185
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
186
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
187
|
+
mockJWKSResponse(publicJWK);
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
validateToken(token, "https://example.com/.well-known/jwks", {
|
|
191
|
+
audience: "wrong-client",
|
|
192
|
+
}),
|
|
193
|
+
).rejects.toThrow();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should verify token with matching issuer", async () => {
|
|
197
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
198
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
199
|
+
mockJWKSResponse(publicJWK);
|
|
200
|
+
|
|
201
|
+
const result = await validateToken(
|
|
202
|
+
token,
|
|
203
|
+
"https://example.com/.well-known/jwks",
|
|
204
|
+
{ issuer: "https://example.com" },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result).toBeDefined();
|
|
208
|
+
expect(result.payload.iss).toBe("https://example.com");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should reject token with mismatched issuer", async () => {
|
|
212
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
213
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
214
|
+
mockJWKSResponse(publicJWK);
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
validateToken(token, "https://example.com/.well-known/jwks", {
|
|
218
|
+
issuer: "https://wrong-issuer.com",
|
|
219
|
+
}),
|
|
220
|
+
).rejects.toThrow();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should verify token with both audience and issuer", async () => {
|
|
224
|
+
const { publicJWK, privateKey, kid } = await createTestJWKS("RS256");
|
|
225
|
+
const token = await createSignedToken(privateKey, "RS256", kid);
|
|
226
|
+
mockJWKSResponse(publicJWK);
|
|
227
|
+
|
|
228
|
+
const result = await validateToken(
|
|
229
|
+
token,
|
|
230
|
+
"https://example.com/.well-known/jwks",
|
|
231
|
+
{
|
|
232
|
+
audience: "test-client",
|
|
233
|
+
issuer: "https://example.com",
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(result).toBeDefined();
|
|
238
|
+
expect(result.payload.aud).toBe("test-client");
|
|
239
|
+
expect(result.payload.iss).toBe("https://example.com");
|
|
240
|
+
});
|
|
241
|
+
});
|