mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
test(oauth-provider): verify JWKS routing with signed tokens
This commit is contained in:
@@ -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 } },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user