mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(core): reject ID tokens when the provider has no client ID (#11931)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/core": patch
|
||||
---
|
||||
|
||||
ID token sign-in now rejects tokens when the provider has no client ID, because their audience cannot be checked and a token issued to another app would otherwise be accepted. Set `clientId` on the provider to every client ID your apps sign in with.
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { JWK } from "jose";
|
||||
import { createLocalJWKSet, exportJWK, generateKeyPair, SignJWT } from "jose";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { logger } from "../env";
|
||||
import { apple } from "../social-providers/apple";
|
||||
import { cognito } from "../social-providers/cognito";
|
||||
import { facebook } from "../social-providers/facebook";
|
||||
@@ -43,7 +44,7 @@ function providerWith(
|
||||
idToken: OAuthIdTokenConfig | undefined,
|
||||
options: Record<string, unknown> = {},
|
||||
): ProviderWithIdTokenConfig {
|
||||
return { idToken, options };
|
||||
return { id: "test-provider", idToken, options };
|
||||
}
|
||||
|
||||
const sha256Hex = (value: string) =>
|
||||
@@ -259,6 +260,35 @@ describe("verifyProviderIdToken", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("without a configured audience", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each<[string, string | string[] | undefined]>([
|
||||
["missing", undefined],
|
||||
["an empty string", ""],
|
||||
["an empty list", []],
|
||||
["a list of empty strings", [""]],
|
||||
])("rejects a token when the audience is %s", async (_, audience) => {
|
||||
const error = vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const { jwks, sign } = await makeKeyset();
|
||||
const provider = providerWith({
|
||||
jwks,
|
||||
issuer: ISSUER,
|
||||
// Providers pass `clientId` through, which is undefined when it is not set.
|
||||
audience: audience as string | string[],
|
||||
});
|
||||
|
||||
await expect(
|
||||
verifyProviderIdToken(provider, await sign({ sub: "u1" })),
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("no client ID"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider regressions", () => {
|
||||
it("PayPal no longer accepts client id_token sign-in", () => {
|
||||
// Previously verifyIdToken returned true for any decodable token without
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JWTVerifyOptions } from "jose";
|
||||
import { jwtVerify } from "jose";
|
||||
import { logger } from "../env";
|
||||
import type { GenericEndpointContext } from "../types";
|
||||
import type { OAuthProvider, ProviderOptions } from "./oauth-provider";
|
||||
|
||||
@@ -38,7 +39,10 @@ export function getIdTokenVerifyOptions(options: {
|
||||
} satisfies JWTVerifyOptions;
|
||||
}
|
||||
|
||||
type ProviderWithIdTokenConfig = Pick<OAuthProvider, "idToken" | "options">;
|
||||
type ProviderWithIdTokenConfig = Pick<
|
||||
OAuthProvider,
|
||||
"id" | "idToken" | "options"
|
||||
>;
|
||||
|
||||
async function sha256Hex(value: string) {
|
||||
const data = new TextEncoder().encode(value);
|
||||
@@ -124,6 +128,14 @@ export async function verifyProviderIdToken(
|
||||
if (token.split(".").length !== 3) {
|
||||
return config.allowOpaqueToken === true;
|
||||
}
|
||||
// An empty audience would skip the audience check entirely.
|
||||
const hasAudience = [config.audience].flat().some(Boolean);
|
||||
if (!hasAudience) {
|
||||
logger.error(
|
||||
`[better-auth] Rejected an ID token for "${provider.id}" because the provider has no client ID to check its audience against. Set \`clientId\` on the provider.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// `kid` is optional in JWS: a JWKS resolver can still select a key by algorithm, so
|
||||
// key selection is left to config.jwks.
|
||||
const { payload } = await jwtVerify(
|
||||
|
||||
Reference in New Issue
Block a user