From f758f14a4bfee94129f676bb19e87fdd10605dc6 Mon Sep 17 00:00:00 2001 From: Jonathan Samines Date: Mon, 8 Dec 2025 15:49:31 -0600 Subject: [PATCH] chore: add nonce check and verifying jwt claims for google (#6614) --- packages/core/src/social-providers/google.ts | 63 ++++++++++++++------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/core/src/social-providers/google.ts b/packages/core/src/social-providers/google.ts index 522865c40e..58e04b0dd3 100644 --- a/packages/core/src/social-providers/google.ts +++ b/packages/core/src/social-providers/google.ts @@ -1,5 +1,6 @@ import { betterFetch } from "@better-fetch/fetch"; -import { decodeJwt } from "jose"; +import { APIError } from "better-call"; +import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose"; import { logger } from "../env"; import { BetterAuthError } from "../error"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; @@ -126,24 +127,26 @@ export const google = (options: GoogleOptions) => { if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } - const googlePublicKeyUrl = `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}`; - const { data: tokenInfo } = await betterFetch<{ - aud: string; - iss: string; - email: string; - email_verified: boolean; - name: string; - picture: string; - sub: string; - }>(googlePublicKeyUrl); - if (!tokenInfo) { + + // Verify JWT integrity + // See https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token + + const { kid, alg: jwtAlg } = decodeProtectedHeader(token); + if (!kid || !jwtAlg) return false; + + const publicKey = await getGooglePublicKey(kid); + const { payload: jwtClaims } = await jwtVerify(token, publicKey, { + algorithms: [jwtAlg], + issuer: ["https://accounts.google.com", "accounts.google.com"], + audience: options.clientId, + maxTokenAge: "1h", + }); + + if (nonce && jwtClaims.nonce !== nonce) { return false; } - const isValid = - tokenInfo.aud === options.clientId && - (tokenInfo.iss === "https://accounts.google.com" || - tokenInfo.iss === "accounts.google.com"); - return isValid; + + return true; }, async getUserInfo(token) { if (options.getUserInfo) { @@ -169,3 +172,29 @@ export const google = (options: GoogleOptions) => { options, } satisfies OAuthProvider; }; + +export const getGooglePublicKey = async (kid: string) => { + const { data } = await betterFetch<{ + keys: Array<{ + kid: string; + alg: string; + kty: string; + use: string; + n: string; + e: string; + }>; + }>("https://www.googleapis.com/oauth2/v3/certs"); + + if (!data?.keys) { + throw new APIError("BAD_REQUEST", { + message: "Keys not found", + }); + } + + const jwk = data.keys.find((key) => key.kid === kid); + if (!jwk) { + throw new Error(`JWK with kid ${kid} not found`); + } + + return await importJWK(jwk, jwk.alg); +};