fix(core): honor options-level client assertions

This commit is contained in:
Gustavo Valverde
2026-05-16 20:56:42 +01:00
parent 23041d94fd
commit 4a3ede0e29
4 changed files with 141 additions and 6 deletions
@@ -22,9 +22,11 @@ export async function clientCredentialsTokenRequest({
options = typeof options === "function" ? await options() : options;
let extraParams: Record<string, string> | undefined;
if (clientAssertionProvider) {
const resolvedClientAssertionProvider =
clientAssertionProvider ?? options.clientAssertionProvider;
if (resolvedClientAssertionProvider) {
extraParams = await resolveAssertionParams({
clientAssertionProvider,
clientAssertionProvider: resolvedClientAssertionProvider,
});
}
@@ -66,6 +66,24 @@ describe("private_key_jwt OAuth2 helpers", () => {
return body;
}
function expectCustomAssertionRequest(clientAssertion: string) {
const [url, init] = mockedBetterFetch.mock.calls[0] ?? [];
expect(url).toBe(tokenEndpoint);
expect(init?.method).toBe("POST");
expect(init?.headers).not.toHaveProperty("authorization");
const body = init?.body as URLSearchParams;
expect(body).toBeInstanceOf(URLSearchParams);
expect(body.get("client_id")).toBe(clientId);
expect(body.get("client_secret")).toBeNull();
expect(body.get("client_assertion_type")).toBe(
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
);
expect(body.get("client_assertion")).toBe(clientAssertion);
return body;
}
it("should sign authorization-code assertions with the request tokenEndpoint", async () => {
mockedBetterFetch.mockResolvedValueOnce({
data: {
@@ -94,6 +112,34 @@ describe("private_key_jwt OAuth2 helpers", () => {
expect(body.get("redirect_uri")).toBe("https://rp.example.com/callback");
});
it("should use an options-level authorization-code assertion provider", async () => {
const clientAssertionProvider = vi.fn(async () => "options-auth-assertion");
mockedBetterFetch.mockResolvedValueOnce({
data: {
access_token: "access-token",
token_type: "Bearer",
},
error: null,
});
await validateAuthorizationCode({
code: "auth-code",
redirectURI: "https://rp.example.com/callback",
options: {
clientId,
clientSecret: "client-secret",
clientAssertionProvider,
},
tokenEndpoint,
authentication: "basic",
});
expect(clientAssertionProvider).toHaveBeenCalledOnce();
const body = expectCustomAssertionRequest("options-auth-assertion");
expect(body.get("grant_type")).toBe("authorization_code");
expect(body.get("code")).toBe("auth-code");
});
it("should sign refresh-token assertions with the request tokenEndpoint", async () => {
mockedBetterFetch.mockResolvedValueOnce({
data: {
@@ -120,6 +166,35 @@ describe("private_key_jwt OAuth2 helpers", () => {
expect(body.get("refresh_token")).toBe("old-refresh-token");
});
it("should use an options-level refresh-token assertion provider", async () => {
const clientAssertionProvider = vi.fn(
async () => "options-refresh-assertion",
);
mockedBetterFetch.mockResolvedValueOnce({
data: {
access_token: "new-access-token",
token_type: "Bearer",
},
error: null,
});
await refreshAccessToken({
refreshToken: "old-refresh-token",
options: {
clientId,
clientSecret: "client-secret",
clientAssertionProvider,
},
tokenEndpoint,
authentication: "basic",
});
expect(clientAssertionProvider).toHaveBeenCalledOnce();
const body = expectCustomAssertionRequest("options-refresh-assertion");
expect(body.get("grant_type")).toBe("refresh_token");
expect(body.get("refresh_token")).toBe("old-refresh-token");
});
it("should sign client-credentials assertions with the request tokenEndpoint", async () => {
mockedBetterFetch.mockResolvedValueOnce({
data: {
@@ -146,6 +221,37 @@ describe("private_key_jwt OAuth2 helpers", () => {
expect(body.get("scope")).toBe("openid profile");
});
it("should use an options-level client-credentials assertion provider", async () => {
const clientAssertionProvider = vi.fn(
async () => "options-client-credentials-assertion",
);
mockedBetterFetch.mockResolvedValueOnce({
data: {
access_token: "client-credentials-access-token",
token_type: "Bearer",
},
error: null,
});
await clientCredentialsToken({
options: {
clientId,
clientSecret: "client-secret",
clientAssertionProvider,
},
tokenEndpoint,
scope: "openid profile",
authentication: "basic",
});
expect(clientAssertionProvider).toHaveBeenCalledOnce();
const body = expectCustomAssertionRequest(
"options-client-credentials-assertion",
);
expect(body.get("grant_type")).toBe("client_credentials");
expect(body.get("scope")).toBe("openid profile");
});
it("should send a custom client assertion provider result without a client secret", async () => {
const clientAssertionProvider = vi.fn(
async () => "custom-client-assertion",
@@ -192,4 +298,27 @@ describe("private_key_jwt OAuth2 helpers", () => {
);
expect(body.get("client_assertion")).toBe("client-assertion");
});
it("should prefer explicit clientAssertionProvider over options provider", async () => {
const optionsClientAssertionProvider = vi.fn(
async () => "options-client-assertion",
);
const explicitClientAssertionProvider = vi.fn(
async () => "explicit-client-assertion",
);
const { body } = await authorizationCodeRequest({
code: "auth-code",
redirectURI: "https://rp.example.com/callback",
options: {
clientId,
clientAssertionProvider: optionsClientAssertionProvider,
},
clientAssertionProvider: explicitClientAssertionProvider,
});
expect(optionsClientAssertionProvider).not.toHaveBeenCalled();
expect(explicitClientAssertionProvider).toHaveBeenCalledOnce();
expect(body.get("client_assertion")).toBe("explicit-client-assertion");
});
});
@@ -23,9 +23,11 @@ export async function refreshAccessTokenRequest({
}) {
options = typeof options === "function" ? await options() : options;
if (clientAssertionProvider) {
const resolvedClientAssertionProvider =
clientAssertionProvider ?? options.clientAssertionProvider;
if (resolvedClientAssertionProvider) {
const assertionParams = await resolveAssertionParams({
clientAssertionProvider,
clientAssertionProvider: resolvedClientAssertionProvider,
});
extraParams = { ...extraParams, ...assertionParams };
}
@@ -33,9 +33,11 @@ export async function authorizationCodeRequest({
}) {
options = typeof options === "function" ? await options() : options;
if (clientAssertionProvider) {
const resolvedClientAssertionProvider =
clientAssertionProvider ?? options.clientAssertionProvider;
if (resolvedClientAssertionProvider) {
const assertionParams = await resolveAssertionParams({
clientAssertionProvider,
clientAssertionProvider: resolvedClientAssertionProvider,
});
additionalParams = { ...additionalParams, ...assertionParams };
}