mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
fix(core): honor options-level client assertions
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user