test(oauth-provider): verify JWKS routing with signed tokens

This commit is contained in:
Taesu
2026-10-13 09:24:58 +09:00
parent b05b977688
commit de91cc7df0
5 changed files with 28 additions and 123 deletions
@@ -1,4 +1,3 @@
import { verifyBearerToken } from "better-auth/oauth2";
import { jwt } from "better-auth/plugins";
import { getTestInstance } from "better-auth/test";
import type { Auth, BetterAuthOptions } from "better-auth/types";
@@ -6,14 +5,6 @@ import { expect, expectTypeOf, it, vi } from "vitest";
import { oauthProviderResourceClient } from "./client-resource";
import { oauthProvider } from "./oauth";
vi.mock("better-auth/oauth2", async (importOriginal) => {
const original = await importOriginal<typeof import("better-auth/oauth2")>();
return {
...original,
verifyBearerToken: vi.fn(async () => ({ sub: "test-user" })),
};
});
it("accepts auth options that omit base URL configuration", () => {
const options = { plugins: [] } satisfies BetterAuthOptions;
type AuthWithoutBaseURL = Auth<typeof options>;
@@ -24,43 +15,33 @@ it("accepts auth options that omit base URL configuration", () => {
.toEqualTypeOf<AuthWithoutBaseURL | undefined>();
});
it.each([
{
baseURL: "https://auth.example.com/api/auth/",
basePath: "/api/auth",
expectedJWKSURL: "https://auth.example.com/api/auth/keys/jwks.json",
},
{
baseURL: "https://auth.example.com",
basePath: "custom/auth/",
expectedJWKSURL: "https://auth.example.com/custom/auth/keys/jwks.json",
},
{
baseURL: "https://auth.example.com",
basePath: undefined,
expectedJWKSURL: "https://auth.example.com/api/auth/keys/jwks.json",
},
])("resolves the JWKS URL from $baseURL and $basePath", async ({
it.for([
{ baseURL: "https://auth.example.com/api/auth/", basePath: "/api/auth" },
{ baseURL: "https://auth.example.com", basePath: "custom/auth/" },
{ baseURL: "https://auth.example.com", basePath: undefined },
])("verifies a token through the configured JWKS route: $baseURL, $basePath", async ({
baseURL,
basePath,
expectedJWKSURL,
}) => {
const issuer = "https://auth.example.com";
const audience = "https://api.example.com";
const { auth } = await getTestInstance({
baseURL,
basePath,
plugins: [
jwt({ jwks: { jwksPath: "/keys/jwks.json" } }),
jwt({ jwt: { issuer }, jwks: { jwksPath: "/keys/jwks.json" } }),
oauthProvider({ loginPage: "/login", consentPage: "/consent" }),
],
});
const resourceClient = oauthProviderResourceClient(auth);
await resourceClient.getActions().verifyBearerToken("access-token");
expect(verifyBearerToken).toHaveBeenCalledWith(
"access-token",
expect.objectContaining({
jwksUrl: expectedJWKSURL,
}),
vi.spyOn(globalThis, "fetch").mockImplementation((input, init) =>
auth.handler(new Request(input, init)),
);
const { token } = await auth.api.signJWT({
body: { payload: { sub: "test-user", aud: audience } },
});
const client = oauthProviderResourceClient(auth).getActions();
await expect(
client.verifyBearerToken(token, { verifyOptions: { issuer, audience } }),
).resolves.toMatchObject({ sub: "test-user" });
});
@@ -22,24 +22,6 @@ interface TokenErrorBody {
}
describe("oauth-provider device-code composition", () => {
it("advertises the registered device endpoint with a trailing auth base slash", async () => {
const { auth } = await getTestInstance({
baseURL: "http://localhost:3000/api/auth/",
plugins: [
jwt(),
oauthProvider({ loginPage: "/login", consentPage: "/consent" }),
oauthDeviceAuthorization(),
],
});
const metadata = (await auth.api.getOAuthServerConfig()) as unknown as {
device_authorization_endpoint?: string;
};
expect(metadata?.device_authorization_endpoint).toBe(
"http://localhost:3000/api/auth//device/code",
);
});
it("registers Device Authorization and its OAuth grant from one plugin", async () => {
const baseURL = "http://localhost:3000";
const { auth } = await getTestInstance({
@@ -42,17 +42,6 @@ describe("configured UserInfo audience identity", async () => {
audience: "https://Auth.Example.com:443/api/auth/oauth2/userinfo",
active: false,
},
{
audience: "https://other.example.com/api/auth//oauth2/userinfo",
active: false,
},
{
audience: [
"https://Auth.Example.com:443/api/auth//oauth2/userinfo",
"https://other.example.com/api/auth//oauth2/userinfo",
],
active: false,
},
])("reports active=$active for $audience", async ({ audience, active }) => {
const { token } = await auth.api.signJWT({
body: { payload: { azp: client.client_id, aud: audience } },
+4 -11
View File
@@ -133,22 +133,15 @@ describe("oauth metadata", async () => {
expect(oauthMetadata).toMatchObject(metadata ?? {});
});
it("preserves advertised endpoint identities with a trailing base URL slash", async () => {
const { auth } = await createTestInstance({
baseURL: `${baseURL}/`,
});
const metadata = await auth.api.getOpenIdConfig();
expect(metadata?.token_endpoint).toBe(`${baseURL}//oauth2/token`);
expect(metadata?.userinfo_endpoint).toBe(`${baseURL}//oauth2/userinfo`);
});
it("advertises the registered custom JWKS path with a trailing base URL slash", async () => {
it("serves the advertised JWKS without changing endpoint identities", async () => {
const { auth } = await createTestInstance({
baseURL: `${baseURL}/`,
jwtConfig: { jwks: { jwksPath: "/.well-known/jwks.json" } },
});
const metadata = await auth.api.getOpenIdConfig();
expect(metadata?.token_endpoint).toBe(`${baseURL}//oauth2/token`);
expect(metadata?.userinfo_endpoint).toBe(`${baseURL}//oauth2/userinfo`);
const jwksURI = `${baseURL}/.well-known/jwks.json`;
expect(metadata?.jwks_uri).toBe(jwksURI);
@@ -219,62 +219,22 @@ describe("private_key_jwt authentication", async () => {
});
}
it.each([
"/oauth2/token",
"/oauth2/introspect",
"/oauth2/revoke",
])("preserves the configured endpoint audience for %s", async (path) => {
/** @see https://www.rfc-editor.org/rfc/rfc7523.html#section-3 */
it("preserves the default token audience spelling", async () => {
const context = await auth.$context;
const baseURL = "https://Auth.Example.com:443/api/auth/";
const ctx = {
path,
context: { ...context, baseURL },
} as unknown as GenericEndpointContext;
const opts = { loginPage: "/login", consentPage: "/consent" };
const expectedAudience = `${baseURL}${path}`;
const assertion = await signAssertion({ aud: `${baseURL}${path}` });
const ctx = { context: { ...context, baseURL } } as GenericEndpointContext;
const assertion = await signAssertion({ aud: `${baseURL}/oauth2/token` });
await expect(
verifyClientAssertion(
ctx,
opts,
{ loginPage: "/login", consentPage: "/consent" },
assertion,
CLIENT_ASSERTION_TYPE,
assertionClient.client_id,
expectedAudience,
),
).resolves.toEqual({ clientId: assertionClient.client_id });
await expect(
verifyClientAssertion(
ctx,
opts,
assertion,
CLIENT_ASSERTION_TYPE,
assertionClient.client_id,
expectedAudience,
),
).rejects.toMatchObject({ body: { error: "invalid_client" } });
await expect(
verifyClientAssertion(
ctx,
opts,
await signAssertion({
aud: "https://other.example.com/api/auth/oauth2/token",
}),
CLIENT_ASSERTION_TYPE,
assertionClient.client_id,
expectedAudience,
),
).rejects.toMatchObject({ body: { error: "invalid_client" } });
await expect(
verifyClientAssertion(
ctx,
opts,
await signAssertion({ aud: `${baseURL}/oauth2/other` }),
CLIENT_ASSERTION_TYPE,
assertionClient.client_id,
expectedAudience,
),
).rejects.toMatchObject({ body: { error: "invalid_client" } });
});
async function createDiscoveredClientVerifier(input: {