@better-auth/core 1.4.18 → 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.
@@ -39,19 +39,23 @@ const apple = (options) => {
39
39
  async verifyIdToken(token, nonce) {
40
40
  if (options.disableIdTokenSignIn) return false;
41
41
  if (options.verifyIdToken) return options.verifyIdToken(token, nonce);
42
- const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
43
- if (!kid || !jwtAlg) return false;
44
- const { payload: jwtClaims } = await jwtVerify(token, await getApplePublicKey(kid), {
45
- algorithms: [jwtAlg],
46
- issuer: "https://appleid.apple.com",
47
- audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
48
- maxTokenAge: "1h"
49
- });
50
- ["email_verified", "is_private_email"].forEach((field) => {
51
- if (jwtClaims[field] !== void 0) jwtClaims[field] = Boolean(jwtClaims[field]);
52
- });
53
- if (nonce && jwtClaims.nonce !== nonce) return false;
54
- return !!jwtClaims;
42
+ try {
43
+ const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
44
+ if (!kid || !jwtAlg) return false;
45
+ const { payload: jwtClaims } = await jwtVerify(token, await getApplePublicKey(kid), {
46
+ algorithms: [jwtAlg],
47
+ issuer: "https://appleid.apple.com",
48
+ audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
49
+ maxTokenAge: "1h"
50
+ });
51
+ ["email_verified", "is_private_email"].forEach((field) => {
52
+ if (jwtClaims[field] !== void 0) jwtClaims[field] = Boolean(jwtClaims[field]);
53
+ });
54
+ if (nonce && jwtClaims.nonce !== nonce) return false;
55
+ return !!jwtClaims;
56
+ } catch {
57
+ return false;
58
+ }
55
59
  },
56
60
  refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
57
61
  return refreshAccessToken({
@@ -1 +1 @@
1
- {"version":3,"file":"apple.mjs","names":["name: string"],"sources":["../../src/social-providers/apple.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\nexport interface AppleProfile {\n\t/**\n\t * The subject registered claim identifies the principal that’s the subject\n\t * of the identity token. Because this token is for your app, the value is\n\t * the unique identifier for the user.\n\t */\n\tsub: string;\n\t/**\n\t * A String value representing the user's email address.\n\t * The email address is either the user's real email address or the proxy\n\t * address, depending on their status private email relay service.\n\t */\n\temail: string;\n\t/**\n\t * A string or Boolean value that indicates whether the service verifies\n\t * the email. The value can either be a string (\"true\" or \"false\") or a\n\t * Boolean (true or false). The system may not verify email addresses for\n\t * Sign in with Apple at Work & School users, and this claim is \"false\" or\n\t * false for those users.\n\t */\n\temail_verified: true | \"true\";\n\t/**\n\t * A string or Boolean value that indicates whether the email that the user\n\t * shares is the proxy address. The value can either be a string (\"true\" or\n\t * \"false\") or a Boolean (true or false).\n\t */\n\tis_private_email: boolean;\n\t/**\n\t * An Integer value that indicates whether the user appears to be a real\n\t * person. Use the value of this claim to mitigate fraud. The possible\n\t * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For\n\t * more information, see ASUserDetectionStatus. This claim is present only\n\t * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14\n\t * and later. The claim isn’t present or supported for web-based apps.\n\t */\n\treal_user_status: number;\n\t/**\n\t * The user’s full name in the format provided during the authorization\n\t * process.\n\t */\n\tname: string;\n\t/**\n\t * The URL to the user's profile picture.\n\t */\n\tpicture: string;\n\tuser?: AppleNonConformUser | undefined;\n}\n\n/**\n * This is the shape of the `user` query parameter that Apple sends the first\n * time the user consents to the app.\n * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./\n */\nexport interface AppleNonConformUser {\n\tname: {\n\t\tfirstName: string;\n\t\tlastName: string;\n\t};\n\temail: string;\n}\n\nexport interface AppleOptions extends ProviderOptions<AppleProfile> {\n\tclientId: string;\n\tappBundleIdentifier?: string | undefined;\n\taudience?: (string | string[]) | undefined;\n}\n\nexport const apple = (options: AppleOptions) => {\n\tconst tokenEndpoint = \"https://appleid.apple.com/auth/token\";\n\treturn {\n\t\tid: \"apple\",\n\t\tname: \"Apple\",\n\t\tasync createAuthorizationURL({ state, scopes, redirectURI }) {\n\t\t\tconst _scope = options.disableDefaultScope ? [] : [\"email\", \"name\"];\n\t\t\tif (options.scope) _scope.push(...options.scope);\n\t\t\tif (scopes) _scope.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"apple\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://appleid.apple.com/auth/authorize\",\n\t\t\t\tscopes: _scope,\n\t\t\t\tstate,\n\t\t\t\tredirectURI,\n\t\t\t\tresponseMode: \"form_post\",\n\t\t\t\tresponseType: \"code id_token\",\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\t\t\tconst decodedHeader = decodeProtectedHeader(token);\n\t\t\tconst { kid, alg: jwtAlg } = decodedHeader;\n\t\t\tif (!kid || !jwtAlg) return false;\n\t\t\tconst publicKey = await getApplePublicKey(kid);\n\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\tissuer: \"https://appleid.apple.com\",\n\t\t\t\taudience:\n\t\t\t\t\toptions.audience && options.audience.length\n\t\t\t\t\t\t? options.audience\n\t\t\t\t\t\t: options.appBundleIdentifier\n\t\t\t\t\t\t\t? options.appBundleIdentifier\n\t\t\t\t\t\t\t: options.clientId,\n\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t});\n\t\t\t[\"email_verified\", \"is_private_email\"].forEach((field) => {\n\t\t\t\tif (jwtClaims[field] !== undefined) {\n\t\t\t\t\tjwtClaims[field] = Boolean(jwtClaims[field]);\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn !!jwtClaims;\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://appleid.apple.com/auth/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst profile = decodeJwt<AppleProfile>(token.idToken);\n\t\t\tif (!profile) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// TODO: \" \" masking will be removed when the name field is made optional\n\t\t\tlet name: string;\n\t\t\tif (token.user?.name) {\n\t\t\t\tconst firstName = token.user.name.firstName || \"\";\n\t\t\t\tconst lastName = token.user.name.lastName || \"\";\n\t\t\t\tconst fullName = `${firstName} ${lastName}`.trim();\n\t\t\t\tname = fullName || \" \";\n\t\t\t} else {\n\t\t\t\tname = profile.name || \" \";\n\t\t\t}\n\n\t\t\tconst emailVerified =\n\t\t\t\ttypeof profile.email_verified === \"boolean\"\n\t\t\t\t\t? profile.email_verified\n\t\t\t\t\t: profile.email_verified === \"true\";\n\t\t\tconst enrichedProfile = {\n\t\t\t\t...profile,\n\t\t\t\tname,\n\t\t\t};\n\t\t\tconst userMap = await options.mapProfileToUser?.(enrichedProfile);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: profile.sub,\n\t\t\t\t\tname: enrichedProfile.name,\n\t\t\t\t\temailVerified: emailVerified,\n\t\t\t\t\temail: profile.email,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: enrichedProfile,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<AppleProfile>;\n};\n\nexport const getApplePublicKey = async (kid: string) => {\n\tconst APPLE_BASE_URL = \"https://appleid.apple.com\";\n\tconst JWKS_APPLE_URI = \"/auth/keys\";\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;AA4EA,MAAa,SAAS,YAA0B;CAC/C,MAAM,gBAAgB;AACtB,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAAE,OAAO,QAAQ,eAAe;GAC5D,MAAM,SAAS,QAAQ,sBAAsB,EAAE,GAAG,CAAC,SAAS,OAAO;AACnE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,OAAQ,QAAO,KAAK,GAAG,OAAO;AAWlC,UAVY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA,cAAc;IACd,cAAc;IACd,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;GAG3C,MAAM,EAAE,KAAK,KAAK,WADI,sBAAsB,MAAM;AAElD,OAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;GAE5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,kBAAkB,IAAI,EACmB;IAChE,YAAY,CAAC,OAAO;IACpB,QAAQ;IACR,UACC,QAAQ,YAAY,QAAQ,SAAS,SAClC,QAAQ,WACR,QAAQ,sBACP,QAAQ,sBACR,QAAQ;IACb,aAAa;IACb,CAAC;AACF,IAAC,kBAAkB,mBAAmB,CAAC,SAAS,UAAU;AACzD,QAAI,UAAU,WAAW,OACxB,WAAU,SAAS,QAAQ,UAAU,OAAO;KAE5C;AACF,OAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAER,UAAO,CAAC,CAAC;;EAEV,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,UAAU,UAAwB,MAAM,QAAQ;AACtD,OAAI,CAAC,QACJ,QAAO;GAIR,IAAIA;AACJ,OAAI,MAAM,MAAM,KAIf,QADiB,GAFC,MAAM,KAAK,KAAK,aAAa,GAEjB,GADb,MAAM,KAAK,KAAK,YAAY,KACD,MAAM,IAC/B;OAEnB,QAAO,QAAQ,QAAQ;GAGxB,MAAM,gBACL,OAAO,QAAQ,mBAAmB,YAC/B,QAAQ,iBACR,QAAQ,mBAAmB;GAC/B,MAAM,kBAAkB;IACvB,GAAG;IACH;IACA;GACD,MAAM,UAAU,MAAM,QAAQ,mBAAmB,gBAAgB;AACjE,UAAO;IACN,MAAM;KACL,IAAI,QAAQ;KACZ,MAAM,gBAAgB;KACP;KACf,OAAO,QAAQ;KACf,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,oBAAoB,OAAO,QAAgB;CAGvD,MAAM,EAAE,SAAS,MAAM,YASpB,sCAAqC;AACxC,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAEH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAEjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
1
+ {"version":3,"file":"apple.mjs","names":["name: string"],"sources":["../../src/social-providers/apple.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\nexport interface AppleProfile {\n\t/**\n\t * The subject registered claim identifies the principal that’s the subject\n\t * of the identity token. Because this token is for your app, the value is\n\t * the unique identifier for the user.\n\t */\n\tsub: string;\n\t/**\n\t * A String value representing the user's email address.\n\t * The email address is either the user's real email address or the proxy\n\t * address, depending on their status private email relay service.\n\t */\n\temail: string;\n\t/**\n\t * A string or Boolean value that indicates whether the service verifies\n\t * the email. The value can either be a string (\"true\" or \"false\") or a\n\t * Boolean (true or false). The system may not verify email addresses for\n\t * Sign in with Apple at Work & School users, and this claim is \"false\" or\n\t * false for those users.\n\t */\n\temail_verified: true | \"true\";\n\t/**\n\t * A string or Boolean value that indicates whether the email that the user\n\t * shares is the proxy address. The value can either be a string (\"true\" or\n\t * \"false\") or a Boolean (true or false).\n\t */\n\tis_private_email: boolean;\n\t/**\n\t * An Integer value that indicates whether the user appears to be a real\n\t * person. Use the value of this claim to mitigate fraud. The possible\n\t * values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal). For\n\t * more information, see ASUserDetectionStatus. This claim is present only\n\t * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14\n\t * and later. The claim isn’t present or supported for web-based apps.\n\t */\n\treal_user_status: number;\n\t/**\n\t * The user’s full name in the format provided during the authorization\n\t * process.\n\t */\n\tname: string;\n\t/**\n\t * The URL to the user's profile picture.\n\t */\n\tpicture: string;\n\tuser?: AppleNonConformUser | undefined;\n}\n\n/**\n * This is the shape of the `user` query parameter that Apple sends the first\n * time the user consents to the app.\n * @see https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server./\n */\nexport interface AppleNonConformUser {\n\tname: {\n\t\tfirstName: string;\n\t\tlastName: string;\n\t};\n\temail: string;\n}\n\nexport interface AppleOptions extends ProviderOptions<AppleProfile> {\n\tclientId: string;\n\tappBundleIdentifier?: string | undefined;\n\taudience?: (string | string[]) | undefined;\n}\n\nexport const apple = (options: AppleOptions) => {\n\tconst tokenEndpoint = \"https://appleid.apple.com/auth/token\";\n\treturn {\n\t\tid: \"apple\",\n\t\tname: \"Apple\",\n\t\tasync createAuthorizationURL({ state, scopes, redirectURI }) {\n\t\t\tconst _scope = options.disableDefaultScope ? [] : [\"email\", \"name\"];\n\t\t\tif (options.scope) _scope.push(...options.scope);\n\t\t\tif (scopes) _scope.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"apple\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://appleid.apple.com/auth/authorize\",\n\t\t\t\tscopes: _scope,\n\t\t\t\tstate,\n\t\t\t\tredirectURI,\n\t\t\t\tresponseMode: \"form_post\",\n\t\t\t\tresponseType: \"code id_token\",\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst decodedHeader = decodeProtectedHeader(token);\n\t\t\t\tconst { kid, alg: jwtAlg } = decodedHeader;\n\t\t\t\tif (!kid || !jwtAlg) return false;\n\t\t\t\tconst publicKey = await getApplePublicKey(kid);\n\t\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\t\tissuer: \"https://appleid.apple.com\",\n\t\t\t\t\taudience:\n\t\t\t\t\t\toptions.audience && options.audience.length\n\t\t\t\t\t\t\t? options.audience\n\t\t\t\t\t\t\t: options.appBundleIdentifier\n\t\t\t\t\t\t\t\t? options.appBundleIdentifier\n\t\t\t\t\t\t\t\t: options.clientId,\n\t\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t\t});\n\t\t\t\t[\"email_verified\", \"is_private_email\"].forEach((field) => {\n\t\t\t\t\tif (jwtClaims[field] !== undefined) {\n\t\t\t\t\t\tjwtClaims[field] = Boolean(jwtClaims[field]);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn !!jwtClaims;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://appleid.apple.com/auth/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst profile = decodeJwt<AppleProfile>(token.idToken);\n\t\t\tif (!profile) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// TODO: \" \" masking will be removed when the name field is made optional\n\t\t\tlet name: string;\n\t\t\tif (token.user?.name) {\n\t\t\t\tconst firstName = token.user.name.firstName || \"\";\n\t\t\t\tconst lastName = token.user.name.lastName || \"\";\n\t\t\t\tconst fullName = `${firstName} ${lastName}`.trim();\n\t\t\t\tname = fullName || \" \";\n\t\t\t} else {\n\t\t\t\tname = profile.name || \" \";\n\t\t\t}\n\n\t\t\tconst emailVerified =\n\t\t\t\ttypeof profile.email_verified === \"boolean\"\n\t\t\t\t\t? profile.email_verified\n\t\t\t\t\t: profile.email_verified === \"true\";\n\t\t\tconst enrichedProfile = {\n\t\t\t\t...profile,\n\t\t\t\tname,\n\t\t\t};\n\t\t\tconst userMap = await options.mapProfileToUser?.(enrichedProfile);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: profile.sub,\n\t\t\t\t\tname: enrichedProfile.name,\n\t\t\t\t\temailVerified: emailVerified,\n\t\t\t\t\temail: profile.email,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: enrichedProfile,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<AppleProfile>;\n};\n\nexport const getApplePublicKey = async (kid: string) => {\n\tconst APPLE_BASE_URL = \"https://appleid.apple.com\";\n\tconst JWKS_APPLE_URI = \"/auth/keys\";\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;AA4EA,MAAa,SAAS,YAA0B;CAC/C,MAAM,gBAAgB;AACtB,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAAE,OAAO,QAAQ,eAAe;GAC5D,MAAM,SAAS,QAAQ,sBAAsB,EAAE,GAAG,CAAC,SAAS,OAAO;AACnE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,OAAQ,QAAO,KAAK,GAAG,OAAO;AAWlC,UAVY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA,cAAc;IACd,cAAc;IACd,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;AAE3C,OAAI;IAEH,MAAM,EAAE,KAAK,KAAK,WADI,sBAAsB,MAAM;AAElD,QAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;IAE5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,kBAAkB,IAAI,EACmB;KAChE,YAAY,CAAC,OAAO;KACpB,QAAQ;KACR,UACC,QAAQ,YAAY,QAAQ,SAAS,SAClC,QAAQ,WACR,QAAQ,sBACP,QAAQ,sBACR,QAAQ;KACb,aAAa;KACb,CAAC;AACF,KAAC,kBAAkB,mBAAmB,CAAC,SAAS,UAAU;AACzD,SAAI,UAAU,WAAW,OACxB,WAAU,SAAS,QAAQ,UAAU,OAAO;MAE5C;AACF,QAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAER,WAAO,CAAC,CAAC;WACF;AACP,WAAO;;;EAGT,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,UAAU,UAAwB,MAAM,QAAQ;AACtD,OAAI,CAAC,QACJ,QAAO;GAIR,IAAIA;AACJ,OAAI,MAAM,MAAM,KAIf,QADiB,GAFC,MAAM,KAAK,KAAK,aAAa,GAEjB,GADb,MAAM,KAAK,KAAK,YAAY,KACD,MAAM,IAC/B;OAEnB,QAAO,QAAQ,QAAQ;GAGxB,MAAM,gBACL,OAAO,QAAQ,mBAAmB,YAC/B,QAAQ,iBACR,QAAQ,mBAAmB;GAC/B,MAAM,kBAAkB;IACvB,GAAG;IACH;IACA;GACD,MAAM,UAAU,MAAM,QAAQ,mBAAmB,gBAAgB;AACjE,UAAO;IACN,MAAM;KACL,IAAI,QAAQ;KACZ,MAAM,gBAAgB;KACP;KACf,OAAO,QAAQ;KACf,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,oBAAoB,OAAO,QAAgB;CAGvD,MAAM,EAAE,SAAS,MAAM,YASpB,sCAAqC;AACxC,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAEH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAEjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
@@ -66,16 +66,20 @@ const google = (options) => {
66
66
  async verifyIdToken(token, nonce) {
67
67
  if (options.disableIdTokenSignIn) return false;
68
68
  if (options.verifyIdToken) return options.verifyIdToken(token, nonce);
69
- const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
70
- if (!kid || !jwtAlg) return false;
71
- const { payload: jwtClaims } = await jwtVerify(token, await getGooglePublicKey(kid), {
72
- algorithms: [jwtAlg],
73
- issuer: ["https://accounts.google.com", "accounts.google.com"],
74
- audience: options.clientId,
75
- maxTokenAge: "1h"
76
- });
77
- if (nonce && jwtClaims.nonce !== nonce) return false;
78
- return true;
69
+ try {
70
+ const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
71
+ if (!kid || !jwtAlg) return false;
72
+ const { payload: jwtClaims } = await jwtVerify(token, await getGooglePublicKey(kid), {
73
+ algorithms: [jwtAlg],
74
+ issuer: ["https://accounts.google.com", "accounts.google.com"],
75
+ audience: options.clientId,
76
+ maxTokenAge: "1h"
77
+ });
78
+ if (nonce && jwtClaims.nonce !== nonce) return false;
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
79
83
  },
80
84
  async getUserInfo(token) {
81
85
  if (options.getUserInfo) return options.getUserInfo(token);
@@ -1 +1 @@
1
- {"version":3,"file":"google.mjs","names":[],"sources":["../../src/social-providers/google.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { logger } from \"../env\";\nimport { BetterAuthError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\nexport interface GoogleProfile {\n\taud: string;\n\tazp: string;\n\temail: string;\n\temail_verified: boolean;\n\texp: number;\n\t/**\n\t * The family name of the user, or last name in most\n\t * Western languages.\n\t */\n\tfamily_name: string;\n\t/**\n\t * The given name of the user, or first name in most\n\t * Western languages.\n\t */\n\tgiven_name: string;\n\thd?: string | undefined;\n\tiat: number;\n\tiss: string;\n\tjti?: string | undefined;\n\tlocale?: string | undefined;\n\tname: string;\n\tnbf?: number | undefined;\n\tpicture: string;\n\tsub: string;\n}\n\nexport interface GoogleOptions extends ProviderOptions<GoogleProfile> {\n\tclientId: string;\n\t/**\n\t * The access type to use for the authorization code request\n\t */\n\taccessType?: (\"offline\" | \"online\") | undefined;\n\t/**\n\t * The display mode to use for the authorization code request\n\t */\n\tdisplay?: (\"page\" | \"popup\" | \"touch\" | \"wap\") | undefined;\n\t/**\n\t * The hosted domain of the user\n\t */\n\thd?: string | undefined;\n}\n\nexport const google = (options: GoogleOptions) => {\n\treturn {\n\t\tid: \"google\",\n\t\tname: \"Google\",\n\t\tasync createAuthorizationURL({\n\t\t\tstate,\n\t\t\tscopes,\n\t\t\tcodeVerifier,\n\t\t\tredirectURI,\n\t\t\tloginHint,\n\t\t\tdisplay,\n\t\t}) {\n\t\t\tif (!options.clientId || !options.clientSecret) {\n\t\t\t\tlogger.error(\n\t\t\t\t\t\"Client Id and Client Secret is required for Google. Make sure to provide them in the options.\",\n\t\t\t\t);\n\t\t\t\tthrow new BetterAuthError(\"CLIENT_ID_AND_SECRET_REQUIRED\");\n\t\t\t}\n\t\t\tif (!codeVerifier) {\n\t\t\t\tthrow new BetterAuthError(\"codeVerifier is required for Google\");\n\t\t\t}\n\t\t\tconst _scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"email\", \"profile\", \"openid\"];\n\t\t\tif (options.scope) _scopes.push(...options.scope);\n\t\t\tif (scopes) _scopes.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"google\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n\t\t\t\tscopes: _scopes,\n\t\t\t\tstate,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\taccessType: options.accessType,\n\t\t\t\tdisplay: display || options.display,\n\t\t\t\tloginHint,\n\t\t\t\thd: options.hd,\n\t\t\t\tadditionalParams: {\n\t\t\t\t\tinclude_granted_scopes: \"true\",\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t});\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\n\t\t\t// Verify JWT integrity\n\t\t\t// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token\n\n\t\t\tconst { kid, alg: jwtAlg } = decodeProtectedHeader(token);\n\t\t\tif (!kid || !jwtAlg) return false;\n\n\t\t\tconst publicKey = await getGooglePublicKey(kid);\n\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\tissuer: [\"https://accounts.google.com\", \"accounts.google.com\"],\n\t\t\t\taudience: options.clientId,\n\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t});\n\n\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn true;\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as GoogleProfile;\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified: user.email_verified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<GoogleProfile>;\n};\n\nexport const getGooglePublicKey = async (kid: string) => {\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(\"https://www.googleapis.com/oauth2/v3/certs\");\n\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;;;;AAuDA,MAAa,UAAU,YAA2B;AACjD,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAC5B,OACA,QACA,cACA,aACA,WACA,WACE;AACF,OAAI,CAAC,QAAQ,YAAY,CAAC,QAAQ,cAAc;AAC/C,WAAO,MACN,gGACA;AACD,UAAM,IAAI,gBAAgB,gCAAgC;;AAE3D,OAAI,CAAC,aACJ,OAAM,IAAI,gBAAgB,sCAAsC;GAEjE,MAAM,UAAU,QAAQ,sBACrB,EAAE,GACF;IAAC;IAAS;IAAW;IAAS;AACjC,OAAI,QAAQ,MAAO,SAAQ,KAAK,GAAG,QAAQ,MAAM;AACjD,OAAI,OAAQ,SAAQ,KAAK,GAAG,OAAO;AAkBnC,UAjBY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA;IACA,QAAQ,QAAQ;IAChB,YAAY,QAAQ;IACpB,SAAS,WAAW,QAAQ;IAC5B;IACA,IAAI,QAAQ;IACZ,kBAAkB,EACjB,wBAAwB,QACxB;IACD,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA,eAAe;IACf,CAAC;;EAEH,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;GAM3C,MAAM,EAAE,KAAK,KAAK,WAAW,sBAAsB,MAAM;AACzD,OAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;GAG5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,mBAAmB,IAAI,EACkB;IAChE,YAAY,CAAC,OAAO;IACpB,QAAQ,CAAC,+BAA+B,sBAAsB;IAC9D,UAAU,QAAQ;IAClB,aAAa;IACb,CAAC;AAEF,OAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAGR,UAAO;;EAER,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,OAAO,UAAU,MAAM,QAAQ;GACrC,MAAM,UAAU,MAAM,QAAQ,mBAAmB,KAAK;AACtD,UAAO;IACN,MAAM;KACL,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ,eAAe,KAAK;KACpB,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,qBAAqB,OAAO,QAAgB;CACxD,MAAM,EAAE,SAAS,MAAM,YASpB,6CAA6C;AAEhD,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAGH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAGjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
1
+ {"version":3,"file":"google.mjs","names":[],"sources":["../../src/social-providers/google.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { logger } from \"../env\";\nimport { BetterAuthError } from \"../error\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\nexport interface GoogleProfile {\n\taud: string;\n\tazp: string;\n\temail: string;\n\temail_verified: boolean;\n\texp: number;\n\t/**\n\t * The family name of the user, or last name in most\n\t * Western languages.\n\t */\n\tfamily_name: string;\n\t/**\n\t * The given name of the user, or first name in most\n\t * Western languages.\n\t */\n\tgiven_name: string;\n\thd?: string | undefined;\n\tiat: number;\n\tiss: string;\n\tjti?: string | undefined;\n\tlocale?: string | undefined;\n\tname: string;\n\tnbf?: number | undefined;\n\tpicture: string;\n\tsub: string;\n}\n\nexport interface GoogleOptions extends ProviderOptions<GoogleProfile> {\n\tclientId: string;\n\t/**\n\t * The access type to use for the authorization code request\n\t */\n\taccessType?: (\"offline\" | \"online\") | undefined;\n\t/**\n\t * The display mode to use for the authorization code request\n\t */\n\tdisplay?: (\"page\" | \"popup\" | \"touch\" | \"wap\") | undefined;\n\t/**\n\t * The hosted domain of the user\n\t */\n\thd?: string | undefined;\n}\n\nexport const google = (options: GoogleOptions) => {\n\treturn {\n\t\tid: \"google\",\n\t\tname: \"Google\",\n\t\tasync createAuthorizationURL({\n\t\t\tstate,\n\t\t\tscopes,\n\t\t\tcodeVerifier,\n\t\t\tredirectURI,\n\t\t\tloginHint,\n\t\t\tdisplay,\n\t\t}) {\n\t\t\tif (!options.clientId || !options.clientSecret) {\n\t\t\t\tlogger.error(\n\t\t\t\t\t\"Client Id and Client Secret is required for Google. Make sure to provide them in the options.\",\n\t\t\t\t);\n\t\t\t\tthrow new BetterAuthError(\"CLIENT_ID_AND_SECRET_REQUIRED\");\n\t\t\t}\n\t\t\tif (!codeVerifier) {\n\t\t\t\tthrow new BetterAuthError(\"codeVerifier is required for Google\");\n\t\t\t}\n\t\t\tconst _scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"email\", \"profile\", \"openid\"];\n\t\t\tif (options.scope) _scopes.push(...options.scope);\n\t\t\tif (scopes) _scopes.push(...scopes);\n\t\t\tconst url = await createAuthorizationURL({\n\t\t\t\tid: \"google\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n\t\t\t\tscopes: _scopes,\n\t\t\t\tstate,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\taccessType: options.accessType,\n\t\t\t\tdisplay: display || options.display,\n\t\t\t\tloginHint,\n\t\t\t\thd: options.hd,\n\t\t\t\tadditionalParams: {\n\t\t\t\t\tinclude_granted_scopes: \"true\",\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn url;\n\t\t},\n\t\tvalidateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t});\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientKey: options.clientKey,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint: \"https://oauth2.googleapis.com/token\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\n\t\t\t// Verify JWT integrity\n\t\t\t// See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token\n\n\t\t\ttry {\n\t\t\t\tconst { kid, alg: jwtAlg } = decodeProtectedHeader(token);\n\t\t\t\tif (!kid || !jwtAlg) return false;\n\n\t\t\t\tconst publicKey = await getGooglePublicKey(kid);\n\t\t\t\tconst { payload: jwtClaims } = await jwtVerify(token, publicKey, {\n\t\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\t\tissuer: [\"https://accounts.google.com\", \"accounts.google.com\"],\n\t\t\t\t\taudience: options.clientId,\n\t\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t\t});\n\n\t\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as GoogleProfile;\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified: user.email_verified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider<GoogleProfile>;\n};\n\nexport const getGooglePublicKey = async (kid: string) => {\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t}>;\n\t}>(\"https://www.googleapis.com/oauth2/v3/certs\");\n\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;;;;AAuDA,MAAa,UAAU,YAA2B;AACjD,QAAO;EACN,IAAI;EACJ,MAAM;EACN,MAAM,uBAAuB,EAC5B,OACA,QACA,cACA,aACA,WACA,WACE;AACF,OAAI,CAAC,QAAQ,YAAY,CAAC,QAAQ,cAAc;AAC/C,WAAO,MACN,gGACA;AACD,UAAM,IAAI,gBAAgB,gCAAgC;;AAE3D,OAAI,CAAC,aACJ,OAAM,IAAI,gBAAgB,sCAAsC;GAEjE,MAAM,UAAU,QAAQ,sBACrB,EAAE,GACF;IAAC;IAAS;IAAW;IAAS;AACjC,OAAI,QAAQ,MAAO,SAAQ,KAAK,GAAG,QAAQ,MAAM;AACjD,OAAI,OAAQ,SAAQ,KAAK,GAAG,OAAO;AAkBnC,UAjBY,MAAM,uBAAuB;IACxC,IAAI;IACJ;IACA,uBAAuB;IACvB,QAAQ;IACR;IACA;IACA;IACA,QAAQ,QAAQ;IAChB,YAAY,QAAQ;IACpB,SAAS,WAAW,QAAQ;IAC5B;IACA,IAAI,QAAQ;IACZ,kBAAkB,EACjB,wBAAwB,QACxB;IACD,CAAC;;EAGH,2BAA2B,OAAO,EAAE,MAAM,cAAc,kBAAkB;AACzE,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA,eAAe;IACf,CAAC;;EAEH,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;AACxB,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,WAAW,QAAQ;KACnB,cAAc,QAAQ;KACtB;IACD,eAAe;IACf,CAAC;;EAEL,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;AAM3C,OAAI;IACH,MAAM,EAAE,KAAK,KAAK,WAAW,sBAAsB,MAAM;AACzD,QAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;IAG5B,MAAM,EAAE,SAAS,cAAc,MAAM,UAAU,OAD7B,MAAM,mBAAmB,IAAI,EACkB;KAChE,YAAY,CAAC,OAAO;KACpB,QAAQ,CAAC,+BAA+B,sBAAsB;KAC9D,UAAU,QAAQ;KAClB,aAAa;KACb,CAAC;AAEF,QAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAGR,WAAO;WACA;AACP,WAAO;;;EAGT,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,OAAO,UAAU,MAAM,QAAQ;GACrC,MAAM,UAAU,MAAM,QAAQ,mBAAmB,KAAK;AACtD,UAAO;IACN,MAAM;KACL,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ,eAAe,KAAK;KACpB,GAAG;KACH;IACD,MAAM;IACN;;EAEF;EACA;;AAGF,MAAa,qBAAqB,OAAO,QAAgB;CACxD,MAAM,EAAE,SAAS,MAAM,YASpB,6CAA6C;AAEhD,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAGH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAGjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
@@ -7,7 +7,7 @@ import { DiscordOptions, DiscordProfile, discord } from "./discord.mjs";
7
7
  import { FacebookOptions, FacebookProfile, facebook } from "./facebook.mjs";
8
8
  import { FigmaOptions, FigmaProfile, figma } from "./figma.mjs";
9
9
  import { GithubOptions, GithubProfile, github } from "./github.mjs";
10
- import { MicrosoftEntraIDProfile, MicrosoftOptions, microsoft } from "./microsoft-entra-id.mjs";
10
+ import { MicrosoftEntraIDProfile, MicrosoftOptions, getMicrosoftPublicKey, microsoft } from "./microsoft-entra-id.mjs";
11
11
  import { GoogleOptions, GoogleProfile, getGooglePublicKey, google } from "./google.mjs";
12
12
  import { HuggingFaceOptions, HuggingFaceProfile, huggingface } from "./huggingface.mjs";
13
13
  import { SlackOptions, SlackProfile, slack } from "./slack.mjs";
@@ -394,6 +394,7 @@ declare const socialProviders: {
394
394
  codeVerifier?: string | undefined;
395
395
  deviceId?: string | undefined;
396
396
  }): Promise<OAuth2Tokens>;
397
+ verifyIdToken(token: string, nonce: string | undefined): Promise<boolean>;
397
398
  getUserInfo(token: OAuth2Tokens & {
398
399
  user?: {
399
400
  name?: {
@@ -1721,5 +1722,5 @@ type SocialProviders = { [K in SocialProviderList[number]]?: Parameters<(typeof
1721
1722
  } };
1722
1723
  type SocialProviderList = typeof socialProviderList;
1723
1724
  //#endregion
1724
- export { AccountStatus, AppleNonConformUser, AppleOptions, AppleProfile, AtlassianOptions, AtlassianProfile, CognitoOptions, CognitoProfile, DiscordOptions, DiscordProfile, DropboxOptions, DropboxProfile, FacebookOptions, FacebookProfile, FigmaOptions, FigmaProfile, GithubOptions, GithubProfile, GitlabOptions, GitlabProfile, GoogleOptions, GoogleProfile, HuggingFaceOptions, HuggingFaceProfile, KakaoOptions, KakaoProfile, KickOptions, KickProfile, LineIdTokenPayload, LineOptions, LineUserInfo, LinearOptions, LinearProfile, LinearUser, LinkedInOptions, LinkedInProfile, LoginType, MicrosoftEntraIDProfile, MicrosoftOptions, NaverOptions, NaverProfile, NotionOptions, NotionProfile, PayPalOptions, PayPalProfile, PayPalTokenResponse, PaybinOptions, PaybinProfile, PhoneNumber, PolarOptions, PolarProfile, PronounOption, RedditOptions, RedditProfile, RobloxOptions, RobloxProfile, SalesforceOptions, SalesforceProfile, SlackOptions, SlackProfile, SocialProvider, SocialProviderList, SocialProviderListEnum, SocialProviders, SpotifyOptions, SpotifyProfile, TiktokOptions, TiktokProfile, TwitchOptions, TwitchProfile, TwitterOption, TwitterProfile, VercelOptions, VercelProfile, VkOption, VkProfile, ZoomOptions, ZoomProfile, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, getGooglePublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paybin, paypal, polar, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vercel, vk, zoom };
1725
+ export { AccountStatus, AppleNonConformUser, AppleOptions, AppleProfile, AtlassianOptions, AtlassianProfile, CognitoOptions, CognitoProfile, DiscordOptions, DiscordProfile, DropboxOptions, DropboxProfile, FacebookOptions, FacebookProfile, FigmaOptions, FigmaProfile, GithubOptions, GithubProfile, GitlabOptions, GitlabProfile, GoogleOptions, GoogleProfile, HuggingFaceOptions, HuggingFaceProfile, KakaoOptions, KakaoProfile, KickOptions, KickProfile, LineIdTokenPayload, LineOptions, LineUserInfo, LinearOptions, LinearProfile, LinearUser, LinkedInOptions, LinkedInProfile, LoginType, MicrosoftEntraIDProfile, MicrosoftOptions, NaverOptions, NaverProfile, NotionOptions, NotionProfile, PayPalOptions, PayPalProfile, PayPalTokenResponse, PaybinOptions, PaybinProfile, PhoneNumber, PolarOptions, PolarProfile, PronounOption, RedditOptions, RedditProfile, RobloxOptions, RobloxProfile, SalesforceOptions, SalesforceProfile, SlackOptions, SlackProfile, SocialProvider, SocialProviderList, SocialProviderListEnum, SocialProviders, SpotifyOptions, SpotifyProfile, TiktokOptions, TiktokProfile, TwitchOptions, TwitchProfile, TwitterOption, TwitterProfile, VercelOptions, VercelProfile, VkOption, VkProfile, ZoomOptions, ZoomProfile, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, getGooglePublicKey, getMicrosoftPublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paybin, paypal, polar, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vercel, vk, zoom };
1725
1726
  //# sourceMappingURL=index.d.mts.map
@@ -14,7 +14,7 @@ import { kick } from "./kick.mjs";
14
14
  import { line } from "./line.mjs";
15
15
  import { linear } from "./linear.mjs";
16
16
  import { linkedin } from "./linkedin.mjs";
17
- import { microsoft } from "./microsoft-entra-id.mjs";
17
+ import { getMicrosoftPublicKey, microsoft } from "./microsoft-entra-id.mjs";
18
18
  import { naver } from "./naver.mjs";
19
19
  import { notion } from "./notion.mjs";
20
20
  import { paybin } from "./paybin.mjs";
@@ -73,5 +73,5 @@ const socialProviderList = Object.keys(socialProviders);
73
73
  const SocialProviderListEnum = z.enum(socialProviderList).or(z.string());
74
74
 
75
75
  //#endregion
76
- export { SocialProviderListEnum, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, getGooglePublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paybin, paypal, polar, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vercel, vk, zoom };
76
+ export { SocialProviderListEnum, apple, atlassian, cognito, discord, dropbox, facebook, figma, getApplePublicKey, getCognitoPublicKey, getGooglePublicKey, getMicrosoftPublicKey, github, gitlab, google, huggingface, kakao, kick, line, linear, linkedin, microsoft, naver, notion, paybin, paypal, polar, reddit, roblox, salesforce, slack, socialProviderList, socialProviders, spotify, tiktok, twitch, twitter, vercel, vk, zoom };
77
77
  //# sourceMappingURL=index.mjs.map
@@ -148,6 +148,7 @@ declare const microsoft: (options: MicrosoftOptions) => {
148
148
  codeVerifier?: string | undefined;
149
149
  deviceId?: string | undefined;
150
150
  }): Promise<OAuth2Tokens>;
151
+ verifyIdToken(token: string, nonce: string | undefined): Promise<boolean>;
151
152
  getUserInfo(token: OAuth2Tokens & {
152
153
  user?: {
153
154
  name?: {
@@ -170,6 +171,7 @@ declare const microsoft: (options: MicrosoftOptions) => {
170
171
  refreshAccessToken: (refreshToken: string) => Promise<OAuth2Tokens>;
171
172
  options: MicrosoftOptions;
172
173
  };
174
+ declare const getMicrosoftPublicKey: (kid: string, tenant: string, authority: string) => Promise<Uint8Array<ArrayBufferLike> | CryptoKey>;
173
175
  //#endregion
174
- export { MicrosoftEntraIDProfile, MicrosoftOptions, microsoft };
176
+ export { MicrosoftEntraIDProfile, MicrosoftOptions, getMicrosoftPublicKey, microsoft };
175
177
  //# sourceMappingURL=microsoft-entra-id.d.mts.map
@@ -6,7 +6,8 @@ import { validateAuthorizationCode } from "../oauth2/validate-authorization-code
6
6
  import "../oauth2/index.mjs";
7
7
  import { base64 } from "@better-auth/utils/base64";
8
8
  import { betterFetch } from "@better-fetch/fetch";
9
- import { decodeJwt } from "jose";
9
+ import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
10
+ import { APIError } from "better-call";
10
11
 
11
12
  //#region src/social-providers/microsoft-entra-id.ts
12
13
  const microsoft = (options) => {
@@ -48,6 +49,31 @@ const microsoft = (options) => {
48
49
  tokenEndpoint
49
50
  });
50
51
  },
52
+ async verifyIdToken(token, nonce) {
53
+ if (options.disableIdTokenSignIn) return false;
54
+ if (options.verifyIdToken) return options.verifyIdToken(token, nonce);
55
+ try {
56
+ const { kid, alg: jwtAlg } = decodeProtectedHeader(token);
57
+ if (!kid || !jwtAlg) return false;
58
+ const publicKey = await getMicrosoftPublicKey(kid, tenant, authority);
59
+ const verifyOptions = {
60
+ algorithms: [jwtAlg],
61
+ audience: options.clientId,
62
+ maxTokenAge: "1h"
63
+ };
64
+ /**
65
+ * Issuer varies per user's tenant for multi-tenant endpoints, so only validate for specific tenants.
66
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
67
+ */
68
+ if (tenant !== "common" && tenant !== "organizations" && tenant !== "consumers") verifyOptions.issuer = `${authority}/${tenant}/v2.0`;
69
+ const { payload: jwtClaims } = await jwtVerify(token, publicKey, verifyOptions);
70
+ if (nonce && jwtClaims.nonce !== nonce) return false;
71
+ return true;
72
+ } catch (error) {
73
+ logger.error("Failed to verify ID token:", error);
74
+ return false;
75
+ }
76
+ },
51
77
  async getUserInfo(token) {
52
78
  if (options.getUserInfo) return options.getUserInfo(token);
53
79
  if (!token.idToken) return null;
@@ -101,7 +127,14 @@ const microsoft = (options) => {
101
127
  options
102
128
  };
103
129
  };
130
+ const getMicrosoftPublicKey = async (kid, tenant, authority) => {
131
+ const { data } = await betterFetch(`${authority}/${tenant}/discovery/v2.0/keys`);
132
+ if (!data?.keys) throw new APIError("BAD_REQUEST", { message: "Keys not found" });
133
+ const jwk = data.keys.find((key) => key.kid === kid);
134
+ if (!jwk) throw new Error(`JWK with kid ${kid} not found`);
135
+ return await importJWK(jwk, jwk.alg);
136
+ };
104
137
 
105
138
  //#endregion
106
- export { microsoft };
139
+ export { getMicrosoftPublicKey, microsoft };
107
140
  //# sourceMappingURL=microsoft-entra-id.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"microsoft-entra-id.mjs","names":[],"sources":["../../src/social-providers/microsoft-entra-id.ts"],"sourcesContent":["import { base64 } from \"@better-auth/utils/base64\";\nimport { betterFetch } from \"@better-fetch/fetch\";\nimport { decodeJwt } from \"jose\";\nimport { logger } from \"../env\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\n/**\n * @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference)\n */\nexport interface MicrosoftEntraIDProfile extends Record<string, any> {\n\t/** Identifies the intended recipient of the token */\n\taud: string;\n\t/** Identifies the issuer, or \"authorization server\" that constructs and returns the token */\n\tiss: string;\n\t/** Indicates when the authentication for the token occurred */\n\tiat: Date;\n\t/** Records the identity provider that authenticated the subject of the token */\n\tidp: string;\n\t/** Identifies the time before which the JWT can't be accepted for processing */\n\tnbf: Date;\n\t/** Identifies the expiration time on or after which the JWT can't be accepted for processing */\n\texp: Date;\n\t/** Code hash included in ID tokens when issued with an OAuth 2.0 authorization code */\n\tc_hash: string;\n\t/** Access token hash included in ID tokens when issued with an OAuth 2.0 access token */\n\tat_hash: string;\n\t/** Internal claim used to record data for token reuse */\n\taio: string;\n\t/** The primary username that represents the user */\n\tpreferred_username: string;\n\t/** User's email address */\n\temail: string;\n\t/** Human-readable value that identifies the subject of the token */\n\tname: string;\n\t/** Matches the parameter included in the original authorize request */\n\tnonce: string;\n\t/** User's profile picture */\n\tpicture: string;\n\t/** Immutable identifier for the user account */\n\toid: string;\n\t/** Set of roles assigned to the user */\n\troles: string[];\n\t/** Internal claim used to revalidate tokens */\n\trh: string;\n\t/** Subject identifier - unique to application ID */\n\tsub: string;\n\t/** Tenant ID the user is signing in to */\n\ttid: string;\n\t/** Unique identifier for a session */\n\tsid: string;\n\t/** Token identifier claim */\n\tuti: string;\n\t/** Indicates if user is in at least one group */\n\thasgroups: boolean;\n\t/** User account status in tenant (0 = member, 1 = guest) */\n\tacct: 0 | 1;\n\t/** Auth Context IDs */\n\tacrs: string;\n\t/** Time when the user last authenticated */\n\tauth_time: Date;\n\t/** User's country/region */\n\tctry: string;\n\t/** IP address of requesting client when inside VNET */\n\tfwd: string;\n\t/** Group claims */\n\tgroups: string;\n\t/** Login hint for SSO */\n\tlogin_hint: string;\n\t/** Resource tenant's country/region */\n\ttenant_ctry: string;\n\t/** Region of the resource tenant */\n\ttenant_region_scope: string;\n\t/** UserPrincipalName */\n\tupn: string;\n\t/** User's verified primary email addresses */\n\tverified_primary_email: string[];\n\t/** User's verified secondary email addresses */\n\tverified_secondary_email: string[];\n\t/** Whether the user's email is verified (optional claim, must be configured in app registration) */\n\temail_verified?: boolean | undefined;\n\t/** VNET specifier information */\n\tvnet: string;\n\t/** Client Capabilities */\n\txms_cc: string;\n\t/** Whether user's email domain is verified */\n\txms_edov: boolean;\n\t/** Preferred data location for Multi-Geo tenants */\n\txms_pdl: string;\n\t/** User preferred language */\n\txms_pl: string;\n\t/** Tenant preferred language */\n\txms_tpl: string;\n\t/** Zero-touch Deployment ID */\n\tztdid: string;\n\t/** IP Address */\n\tipaddr: string;\n\t/** On-premises Security Identifier */\n\tonprem_sid: string;\n\t/** Password Expiration Time */\n\tpwd_exp: number;\n\t/** Change Password URL */\n\tpwd_url: string;\n\t/** Inside Corporate Network flag */\n\tin_corp: string;\n\t/** User's family name/surname */\n\tfamily_name: string;\n\t/** User's given/first name */\n\tgiven_name: string;\n}\n\nexport interface MicrosoftOptions\n\textends ProviderOptions<MicrosoftEntraIDProfile> {\n\tclientId: string;\n\t/**\n\t * The tenant ID of the Microsoft account\n\t * @default \"common\"\n\t */\n\ttenantId?: string | undefined;\n\t/**\n\t * The authentication authority URL. Use the default \"https://login.microsoftonline.com\" for standard Entra ID or \"https://<tenant-id>.ciamlogin.com\" for CIAM scenarios.\n\t * @default \"https://login.microsoftonline.com\"\n\t */\n\tauthority?: string | undefined;\n\t/**\n\t * The size of the profile photo\n\t * @default 48\n\t */\n\tprofilePhotoSize?:\n\t\t| (48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648)\n\t\t| undefined;\n\t/**\n\t * Disable profile photo\n\t */\n\tdisableProfilePhoto?: boolean | undefined;\n}\n\nexport const microsoft = (options: MicrosoftOptions) => {\n\tconst tenant = options.tenantId || \"common\";\n\tconst authority = options.authority || \"https://login.microsoftonline.com\";\n\tconst authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;\n\tconst tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;\n\treturn {\n\t\tid: \"microsoft\",\n\t\tname: \"Microsoft EntraID\",\n\t\tcreateAuthorizationURL(data) {\n\t\t\tconst scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"openid\", \"profile\", \"email\", \"User.Read\", \"offline_access\"];\n\t\t\tif (options.scope) scopes.push(...options.scope);\n\t\t\tif (data.scopes) scopes.push(...data.scopes);\n\t\t\treturn createAuthorizationURL({\n\t\t\t\tid: \"microsoft\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint,\n\t\t\t\tstate: data.state,\n\t\t\t\tcodeVerifier: data.codeVerifier,\n\t\t\t\tscopes,\n\t\t\t\tredirectURI: data.redirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\tloginHint: data.loginHint,\n\t\t\t});\n\t\t},\n\t\tvalidateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as MicrosoftEntraIDProfile;\n\t\t\tconst profilePhotoSize = options.profilePhotoSize || 48;\n\t\t\tawait betterFetch<ArrayBuffer>(\n\t\t\t\t`https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,\n\t\t\t\t{\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${token.accessToken}`,\n\t\t\t\t\t},\n\t\t\t\t\tasync onResponse(context) {\n\t\t\t\t\t\tif (options.disableProfilePhoto || !context.response.ok) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst response = context.response.clone();\n\t\t\t\t\t\t\tconst pictureBuffer = await response.arrayBuffer();\n\t\t\t\t\t\t\tconst pictureBase64 = base64.encode(pictureBuffer);\n\t\t\t\t\t\t\tuser.picture = `data:image/jpeg;base64, ${pictureBase64}`;\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlogger.error(\n\t\t\t\t\t\t\t\te && typeof e === \"object\" && \"name\" in e\n\t\t\t\t\t\t\t\t\t? (e.name as string)\n\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t\te,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\t// Microsoft Entra ID does NOT include email_verified claim by default.\n\t\t\t// It must be configured as an optional claim in the app registration.\n\t\t\t// We default to false when not provided for security consistency.\n\t\t\t// We can also check verified_primary_email/verified_secondary_email arrays as fallback.\n\t\t\tconst emailVerified =\n\t\t\t\tuser.email_verified !== undefined\n\t\t\t\t\t? user.email_verified\n\t\t\t\t\t: user.email &&\n\t\t\t\t\t\t\t(user.verified_primary_email?.includes(user.email) ||\n\t\t\t\t\t\t\t\tuser.verified_secondary_email?.includes(user.email))\n\t\t\t\t\t\t? true\n\t\t\t\t\t\t: false;\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\tconst scopes = options.disableDefaultScope\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [\"openid\", \"profile\", \"email\", \"User.Read\", \"offline_access\"];\n\t\t\t\t\tif (options.scope) scopes.push(...options.scope);\n\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\textraParams: {\n\t\t\t\t\t\t\tscope: scopes.join(\" \"), // Include the scopes in request to microsoft\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider;\n};\n"],"mappings":";;;;;;;;;;;AA6IA,MAAa,aAAa,YAA8B;CACvD,MAAM,SAAS,QAAQ,YAAY;CACnC,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,wBAAwB,GAAG,UAAU,GAAG,OAAO;CACrD,MAAM,gBAAgB,GAAG,UAAU,GAAG,OAAO;AAC7C,QAAO;EACN,IAAI;EACJ,MAAM;EACN,uBAAuB,MAAM;GAC5B,MAAM,SAAS,QAAQ,sBACpB,EAAE,GACF;IAAC;IAAU;IAAW;IAAS;IAAa;IAAiB;AAChE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,KAAK,OAAQ,QAAO,KAAK,GAAG,KAAK,OAAO;AAC5C,UAAO,uBAAuB;IAC7B,IAAI;IACJ;IACA;IACA,OAAO,KAAK;IACZ,cAAc,KAAK;IACnB;IACA,aAAa,KAAK;IAClB,QAAQ,QAAQ;IAChB,WAAW,KAAK;IAChB,CAAC;;EAEH,0BAA0B,EAAE,MAAM,cAAc,eAAe;AAC9D,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,OAAO,UAAU,MAAM,QAAQ;GACrC,MAAM,mBAAmB,QAAQ,oBAAoB;AACrD,SAAM,YACL,8CAA8C,iBAAiB,GAAG,iBAAiB,UACnF;IACC,SAAS,EACR,eAAe,UAAU,MAAM,eAC/B;IACD,MAAM,WAAW,SAAS;AACzB,SAAI,QAAQ,uBAAuB,CAAC,QAAQ,SAAS,GACpD;AAED,SAAI;MAEH,MAAM,gBAAgB,MADL,QAAQ,SAAS,OAAO,CACJ,aAAa;AAElD,WAAK,UAAU,2BADO,OAAO,OAAO,cAAc;cAE1C,GAAG;AACX,aAAO,MACN,KAAK,OAAO,MAAM,YAAY,UAAU,IACpC,EAAE,OACH,IACH,EACA;;;IAGH,CACD;GACD,MAAM,UAAU,MAAM,QAAQ,mBAAmB,KAAK;GAKtD,MAAM,gBACL,KAAK,mBAAmB,SACrB,KAAK,iBACL,KAAK,UACJ,KAAK,wBAAwB,SAAS,KAAK,MAAM,IACjD,KAAK,0BAA0B,SAAS,KAAK,MAAM,IACnD,OACA;AACL,UAAO;IACN,MAAM;KACL,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ;KACA,GAAG;KACH;IACD,MAAM;IACN;;EAEF,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;GACxB,MAAM,SAAS,QAAQ,sBACpB,EAAE,GACF;IAAC;IAAU;IAAW;IAAS;IAAa;IAAiB;AAChE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAEhD,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,cAAc,QAAQ;KACtB;IACD,aAAa,EACZ,OAAO,OAAO,KAAK,IAAI,EACvB;IACD;IACA,CAAC;;EAEL;EACA"}
1
+ {"version":3,"file":"microsoft-entra-id.mjs","names":["verifyOptions: {\n\t\t\t\t\talgorithms: [string];\n\t\t\t\t\taudience: string;\n\t\t\t\t\tmaxTokenAge: string;\n\t\t\t\t\tissuer?: string;\n\t\t\t\t}"],"sources":["../../src/social-providers/microsoft-entra-id.ts"],"sourcesContent":["import { base64 } from \"@better-auth/utils/base64\";\nimport { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from \"jose\";\nimport { logger } from \"../env\";\nimport type { OAuthProvider, ProviderOptions } from \"../oauth2\";\nimport {\n\tcreateAuthorizationURL,\n\trefreshAccessToken,\n\tvalidateAuthorizationCode,\n} from \"../oauth2\";\n\n/**\n * @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference)\n */\nexport interface MicrosoftEntraIDProfile extends Record<string, any> {\n\t/** Identifies the intended recipient of the token */\n\taud: string;\n\t/** Identifies the issuer, or \"authorization server\" that constructs and returns the token */\n\tiss: string;\n\t/** Indicates when the authentication for the token occurred */\n\tiat: Date;\n\t/** Records the identity provider that authenticated the subject of the token */\n\tidp: string;\n\t/** Identifies the time before which the JWT can't be accepted for processing */\n\tnbf: Date;\n\t/** Identifies the expiration time on or after which the JWT can't be accepted for processing */\n\texp: Date;\n\t/** Code hash included in ID tokens when issued with an OAuth 2.0 authorization code */\n\tc_hash: string;\n\t/** Access token hash included in ID tokens when issued with an OAuth 2.0 access token */\n\tat_hash: string;\n\t/** Internal claim used to record data for token reuse */\n\taio: string;\n\t/** The primary username that represents the user */\n\tpreferred_username: string;\n\t/** User's email address */\n\temail: string;\n\t/** Human-readable value that identifies the subject of the token */\n\tname: string;\n\t/** Matches the parameter included in the original authorize request */\n\tnonce: string;\n\t/** User's profile picture */\n\tpicture: string;\n\t/** Immutable identifier for the user account */\n\toid: string;\n\t/** Set of roles assigned to the user */\n\troles: string[];\n\t/** Internal claim used to revalidate tokens */\n\trh: string;\n\t/** Subject identifier - unique to application ID */\n\tsub: string;\n\t/** Tenant ID the user is signing in to */\n\ttid: string;\n\t/** Unique identifier for a session */\n\tsid: string;\n\t/** Token identifier claim */\n\tuti: string;\n\t/** Indicates if user is in at least one group */\n\thasgroups: boolean;\n\t/** User account status in tenant (0 = member, 1 = guest) */\n\tacct: 0 | 1;\n\t/** Auth Context IDs */\n\tacrs: string;\n\t/** Time when the user last authenticated */\n\tauth_time: Date;\n\t/** User's country/region */\n\tctry: string;\n\t/** IP address of requesting client when inside VNET */\n\tfwd: string;\n\t/** Group claims */\n\tgroups: string;\n\t/** Login hint for SSO */\n\tlogin_hint: string;\n\t/** Resource tenant's country/region */\n\ttenant_ctry: string;\n\t/** Region of the resource tenant */\n\ttenant_region_scope: string;\n\t/** UserPrincipalName */\n\tupn: string;\n\t/** User's verified primary email addresses */\n\tverified_primary_email: string[];\n\t/** User's verified secondary email addresses */\n\tverified_secondary_email: string[];\n\t/** Whether the user's email is verified (optional claim, must be configured in app registration) */\n\temail_verified?: boolean | undefined;\n\t/** VNET specifier information */\n\tvnet: string;\n\t/** Client Capabilities */\n\txms_cc: string;\n\t/** Whether user's email domain is verified */\n\txms_edov: boolean;\n\t/** Preferred data location for Multi-Geo tenants */\n\txms_pdl: string;\n\t/** User preferred language */\n\txms_pl: string;\n\t/** Tenant preferred language */\n\txms_tpl: string;\n\t/** Zero-touch Deployment ID */\n\tztdid: string;\n\t/** IP Address */\n\tipaddr: string;\n\t/** On-premises Security Identifier */\n\tonprem_sid: string;\n\t/** Password Expiration Time */\n\tpwd_exp: number;\n\t/** Change Password URL */\n\tpwd_url: string;\n\t/** Inside Corporate Network flag */\n\tin_corp: string;\n\t/** User's family name/surname */\n\tfamily_name: string;\n\t/** User's given/first name */\n\tgiven_name: string;\n}\n\nexport interface MicrosoftOptions\n\textends ProviderOptions<MicrosoftEntraIDProfile> {\n\tclientId: string;\n\t/**\n\t * The tenant ID of the Microsoft account\n\t * @default \"common\"\n\t */\n\ttenantId?: string | undefined;\n\t/**\n\t * The authentication authority URL. Use the default \"https://login.microsoftonline.com\" for standard Entra ID or \"https://<tenant-id>.ciamlogin.com\" for CIAM scenarios.\n\t * @default \"https://login.microsoftonline.com\"\n\t */\n\tauthority?: string | undefined;\n\t/**\n\t * The size of the profile photo\n\t * @default 48\n\t */\n\tprofilePhotoSize?:\n\t\t| (48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648)\n\t\t| undefined;\n\t/**\n\t * Disable profile photo\n\t */\n\tdisableProfilePhoto?: boolean | undefined;\n}\n\nexport const microsoft = (options: MicrosoftOptions) => {\n\tconst tenant = options.tenantId || \"common\";\n\tconst authority = options.authority || \"https://login.microsoftonline.com\";\n\tconst authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`;\n\tconst tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;\n\treturn {\n\t\tid: \"microsoft\",\n\t\tname: \"Microsoft EntraID\",\n\t\tcreateAuthorizationURL(data) {\n\t\t\tconst scopes = options.disableDefaultScope\n\t\t\t\t? []\n\t\t\t\t: [\"openid\", \"profile\", \"email\", \"User.Read\", \"offline_access\"];\n\t\t\tif (options.scope) scopes.push(...options.scope);\n\t\t\tif (data.scopes) scopes.push(...data.scopes);\n\t\t\treturn createAuthorizationURL({\n\t\t\t\tid: \"microsoft\",\n\t\t\t\toptions,\n\t\t\t\tauthorizationEndpoint,\n\t\t\t\tstate: data.state,\n\t\t\t\tcodeVerifier: data.codeVerifier,\n\t\t\t\tscopes,\n\t\t\t\tredirectURI: data.redirectURI,\n\t\t\t\tprompt: options.prompt,\n\t\t\t\tloginHint: data.loginHint,\n\t\t\t});\n\t\t},\n\t\tvalidateAuthorizationCode({ code, codeVerifier, redirectURI }) {\n\t\t\treturn validateAuthorizationCode({\n\t\t\t\tcode,\n\t\t\t\tcodeVerifier,\n\t\t\t\tredirectURI,\n\t\t\t\toptions,\n\t\t\t\ttokenEndpoint,\n\t\t\t});\n\t\t},\n\t\tasync verifyIdToken(token, nonce) {\n\t\t\tif (options.disableIdTokenSignIn) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (options.verifyIdToken) {\n\t\t\t\treturn options.verifyIdToken(token, nonce);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst { kid, alg: jwtAlg } = decodeProtectedHeader(token);\n\t\t\t\tif (!kid || !jwtAlg) return false;\n\n\t\t\t\tconst publicKey = await getMicrosoftPublicKey(kid, tenant, authority);\n\t\t\t\tconst verifyOptions: {\n\t\t\t\t\talgorithms: [string];\n\t\t\t\t\taudience: string;\n\t\t\t\t\tmaxTokenAge: string;\n\t\t\t\t\tissuer?: string;\n\t\t\t\t} = {\n\t\t\t\t\talgorithms: [jwtAlg],\n\t\t\t\t\taudience: options.clientId,\n\t\t\t\t\tmaxTokenAge: \"1h\",\n\t\t\t\t};\n\t\t\t\t/**\n\t\t\t\t * Issuer varies per user's tenant for multi-tenant endpoints, so only validate for specific tenants.\n\t\t\t\t * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints\n\t\t\t\t */\n\t\t\t\tif (\n\t\t\t\t\ttenant !== \"common\" &&\n\t\t\t\t\ttenant !== \"organizations\" &&\n\t\t\t\t\ttenant !== \"consumers\"\n\t\t\t\t) {\n\t\t\t\t\tverifyOptions.issuer = `${authority}/${tenant}/v2.0`;\n\t\t\t\t}\n\t\t\t\tconst { payload: jwtClaims } = await jwtVerify(\n\t\t\t\t\ttoken,\n\t\t\t\t\tpublicKey,\n\t\t\t\t\tverifyOptions,\n\t\t\t\t);\n\n\t\t\t\tif (nonce && jwtClaims.nonce !== nonce) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t} catch (error) {\n\t\t\t\tlogger.error(\"Failed to verify ID token:\", error);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\tasync getUserInfo(token) {\n\t\t\tif (options.getUserInfo) {\n\t\t\t\treturn options.getUserInfo(token);\n\t\t\t}\n\t\t\tif (!token.idToken) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst user = decodeJwt(token.idToken) as MicrosoftEntraIDProfile;\n\t\t\tconst profilePhotoSize = options.profilePhotoSize || 48;\n\t\t\tawait betterFetch<ArrayBuffer>(\n\t\t\t\t`https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,\n\t\t\t\t{\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${token.accessToken}`,\n\t\t\t\t\t},\n\t\t\t\t\tasync onResponse(context) {\n\t\t\t\t\t\tif (options.disableProfilePhoto || !context.response.ok) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst response = context.response.clone();\n\t\t\t\t\t\t\tconst pictureBuffer = await response.arrayBuffer();\n\t\t\t\t\t\t\tconst pictureBase64 = base64.encode(pictureBuffer);\n\t\t\t\t\t\t\tuser.picture = `data:image/jpeg;base64, ${pictureBase64}`;\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlogger.error(\n\t\t\t\t\t\t\t\te && typeof e === \"object\" && \"name\" in e\n\t\t\t\t\t\t\t\t\t? (e.name as string)\n\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t\te,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst userMap = await options.mapProfileToUser?.(user);\n\t\t\t// Microsoft Entra ID does NOT include email_verified claim by default.\n\t\t\t// It must be configured as an optional claim in the app registration.\n\t\t\t// We default to false when not provided for security consistency.\n\t\t\t// We can also check verified_primary_email/verified_secondary_email arrays as fallback.\n\t\t\tconst emailVerified =\n\t\t\t\tuser.email_verified !== undefined\n\t\t\t\t\t? user.email_verified\n\t\t\t\t\t: user.email &&\n\t\t\t\t\t\t\t(user.verified_primary_email?.includes(user.email) ||\n\t\t\t\t\t\t\t\tuser.verified_secondary_email?.includes(user.email))\n\t\t\t\t\t\t? true\n\t\t\t\t\t\t: false;\n\t\t\treturn {\n\t\t\t\tuser: {\n\t\t\t\t\tid: user.sub,\n\t\t\t\t\tname: user.name,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\timage: user.picture,\n\t\t\t\t\temailVerified,\n\t\t\t\t\t...userMap,\n\t\t\t\t},\n\t\t\t\tdata: user,\n\t\t\t};\n\t\t},\n\t\trefreshAccessToken: options.refreshAccessToken\n\t\t\t? options.refreshAccessToken\n\t\t\t: async (refreshToken) => {\n\t\t\t\t\tconst scopes = options.disableDefaultScope\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [\"openid\", \"profile\", \"email\", \"User.Read\", \"offline_access\"];\n\t\t\t\t\tif (options.scope) scopes.push(...options.scope);\n\n\t\t\t\t\treturn refreshAccessToken({\n\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tclientId: options.clientId,\n\t\t\t\t\t\t\tclientSecret: options.clientSecret,\n\t\t\t\t\t\t},\n\t\t\t\t\t\textraParams: {\n\t\t\t\t\t\t\tscope: scopes.join(\" \"), // Include the scopes in request to microsoft\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttokenEndpoint,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\toptions,\n\t} satisfies OAuthProvider;\n};\n\nexport const getMicrosoftPublicKey = async (\n\tkid: string,\n\ttenant: string,\n\tauthority: string,\n) => {\n\tconst { data } = await betterFetch<{\n\t\tkeys: Array<{\n\t\t\tkid: string;\n\t\t\talg: string;\n\t\t\tkty: string;\n\t\t\tuse: string;\n\t\t\tn: string;\n\t\t\te: string;\n\t\t\tx5c?: string[];\n\t\t\tx5t?: string;\n\t\t}>;\n\t}>(`${authority}/${tenant}/discovery/v2.0/keys`);\n\n\tif (!data?.keys) {\n\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\tmessage: \"Keys not found\",\n\t\t});\n\t}\n\n\tconst jwk = data.keys.find((key) => key.kid === kid);\n\tif (!jwk) {\n\t\tthrow new Error(`JWK with kid ${kid} not found`);\n\t}\n\n\treturn await importJWK(jwk, jwk.alg);\n};\n"],"mappings":";;;;;;;;;;;;AA8IA,MAAa,aAAa,YAA8B;CACvD,MAAM,SAAS,QAAQ,YAAY;CACnC,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,wBAAwB,GAAG,UAAU,GAAG,OAAO;CACrD,MAAM,gBAAgB,GAAG,UAAU,GAAG,OAAO;AAC7C,QAAO;EACN,IAAI;EACJ,MAAM;EACN,uBAAuB,MAAM;GAC5B,MAAM,SAAS,QAAQ,sBACpB,EAAE,GACF;IAAC;IAAU;IAAW;IAAS;IAAa;IAAiB;AAChE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAChD,OAAI,KAAK,OAAQ,QAAO,KAAK,GAAG,KAAK,OAAO;AAC5C,UAAO,uBAAuB;IAC7B,IAAI;IACJ;IACA;IACA,OAAO,KAAK;IACZ,cAAc,KAAK;IACnB;IACA,aAAa,KAAK;IAClB,QAAQ,QAAQ;IAChB,WAAW,KAAK;IAChB,CAAC;;EAEH,0BAA0B,EAAE,MAAM,cAAc,eAAe;AAC9D,UAAO,0BAA0B;IAChC;IACA;IACA;IACA;IACA;IACA,CAAC;;EAEH,MAAM,cAAc,OAAO,OAAO;AACjC,OAAI,QAAQ,qBACX,QAAO;AAER,OAAI,QAAQ,cACX,QAAO,QAAQ,cAAc,OAAO,MAAM;AAG3C,OAAI;IACH,MAAM,EAAE,KAAK,KAAK,WAAW,sBAAsB,MAAM;AACzD,QAAI,CAAC,OAAO,CAAC,OAAQ,QAAO;IAE5B,MAAM,YAAY,MAAM,sBAAsB,KAAK,QAAQ,UAAU;IACrE,MAAMA,gBAKF;KACH,YAAY,CAAC,OAAO;KACpB,UAAU,QAAQ;KAClB,aAAa;KACb;;;;;AAKD,QACC,WAAW,YACX,WAAW,mBACX,WAAW,YAEX,eAAc,SAAS,GAAG,UAAU,GAAG,OAAO;IAE/C,MAAM,EAAE,SAAS,cAAc,MAAM,UACpC,OACA,WACA,cACA;AAED,QAAI,SAAS,UAAU,UAAU,MAChC,QAAO;AAGR,WAAO;YACC,OAAO;AACf,WAAO,MAAM,8BAA8B,MAAM;AACjD,WAAO;;;EAGT,MAAM,YAAY,OAAO;AACxB,OAAI,QAAQ,YACX,QAAO,QAAQ,YAAY,MAAM;AAElC,OAAI,CAAC,MAAM,QACV,QAAO;GAER,MAAM,OAAO,UAAU,MAAM,QAAQ;GACrC,MAAM,mBAAmB,QAAQ,oBAAoB;AACrD,SAAM,YACL,8CAA8C,iBAAiB,GAAG,iBAAiB,UACnF;IACC,SAAS,EACR,eAAe,UAAU,MAAM,eAC/B;IACD,MAAM,WAAW,SAAS;AACzB,SAAI,QAAQ,uBAAuB,CAAC,QAAQ,SAAS,GACpD;AAED,SAAI;MAEH,MAAM,gBAAgB,MADL,QAAQ,SAAS,OAAO,CACJ,aAAa;AAElD,WAAK,UAAU,2BADO,OAAO,OAAO,cAAc;cAE1C,GAAG;AACX,aAAO,MACN,KAAK,OAAO,MAAM,YAAY,UAAU,IACpC,EAAE,OACH,IACH,EACA;;;IAGH,CACD;GACD,MAAM,UAAU,MAAM,QAAQ,mBAAmB,KAAK;GAKtD,MAAM,gBACL,KAAK,mBAAmB,SACrB,KAAK,iBACL,KAAK,UACJ,KAAK,wBAAwB,SAAS,KAAK,MAAM,IACjD,KAAK,0BAA0B,SAAS,KAAK,MAAM,IACnD,OACA;AACL,UAAO;IACN,MAAM;KACL,IAAI,KAAK;KACT,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ;KACA,GAAG;KACH;IACD,MAAM;IACN;;EAEF,oBAAoB,QAAQ,qBACzB,QAAQ,qBACR,OAAO,iBAAiB;GACxB,MAAM,SAAS,QAAQ,sBACpB,EAAE,GACF;IAAC;IAAU;IAAW;IAAS;IAAa;IAAiB;AAChE,OAAI,QAAQ,MAAO,QAAO,KAAK,GAAG,QAAQ,MAAM;AAEhD,UAAO,mBAAmB;IACzB;IACA,SAAS;KACR,UAAU,QAAQ;KAClB,cAAc,QAAQ;KACtB;IACD,aAAa,EACZ,OAAO,OAAO,KAAK,IAAI,EACvB;IACD;IACA,CAAC;;EAEL;EACA;;AAGF,MAAa,wBAAwB,OACpC,KACA,QACA,cACI;CACJ,MAAM,EAAE,SAAS,MAAM,YAWpB,GAAG,UAAU,GAAG,OAAO,sBAAsB;AAEhD,KAAI,CAAC,MAAM,KACV,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,kBACT,CAAC;CAGH,MAAM,MAAM,KAAK,KAAK,MAAM,QAAQ,IAAI,QAAQ,IAAI;AACpD,KAAI,CAAC,IACJ,OAAM,IAAI,MAAM,gBAAgB,IAAI,YAAY;AAGjD,QAAO,MAAM,UAAU,KAAK,IAAI,IAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/core",
3
- "version": "1.4.18",
3
+ "version": "1.4.19",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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,
@@ -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;
@@ -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(token: string, jwksEndpoint: string) {
128
- const { data, error } = await betterFetch<{
129
- keys: {
130
- kid: string;
131
- kty: string;
132
- use: string;
133
- n: string;
134
- e: string;
135
- x5c: string[];
136
- }[];
137
- }>(jwksEndpoint, {
138
- method: "GET",
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
  }