feat(auth-oauth2): custom parameters for authorization, token, and refresh requests (#629)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-13 09:15:25 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 7d0926c961
commit 7a6d275fdb
16 changed files with 771 additions and 5 deletions
@@ -36,6 +36,12 @@ function formValue(httpRequest: Partial<HttpRequest>, name: string) {
return (httpRequest.body?.form ?? []).find((p: { name: string }) => p.name === name)?.value;
}
function headerValue(httpRequest: Partial<HttpRequest>, name: string) {
return (httpRequest.headers ?? []).find(
(h: { name: string }) => h.name.toLowerCase() === name.toLowerCase(),
)?.value;
}
const baseArgs = {
clientId: "client-123",
accessTokenUrl: "https://auth.example.com/token",
@@ -91,3 +97,102 @@ describe("fetchAccessToken scope handling", () => {
expect(formNames(sent[0]!)).not.toContain("scope");
});
});
describe("fetchAccessToken custom parameters", () => {
test("sends a custom header with the token request", async () => {
const { ctx, sent } = createMockContext();
await fetchAccessToken(ctx, {
...baseArgs,
grantType: "authorization_code",
custom: {
headers: [{ name: "Origin", value: "https://app.example.com" }],
body: [],
},
});
expect(headerValue(sent[0]!, "Origin")).toBe("https://app.example.com");
});
test("keeps the generated headers that are not overridden", async () => {
const { ctx, sent } = createMockContext();
await fetchAccessToken(ctx, {
...baseArgs,
grantType: "client_credentials",
credentialsInBody: false,
custom: {
headers: [{ name: "Origin", value: "https://app.example.com" }],
body: [],
},
});
expect(headerValue(sent[0]!, "User-Agent")).toBe("yaak");
expect(headerValue(sent[0]!, "Content-Type")).toBe("application/x-www-form-urlencoded");
expect(headerValue(sent[0]!, "Accept")).toBe(
"application/x-www-form-urlencoded, application/json",
);
// Basic credentials still go out untouched
expect(headerValue(sent[0]!, "Authorization")).toMatch(/^Basic /);
});
test("replaces a generated header of the same name", async () => {
const { ctx, sent } = createMockContext();
await fetchAccessToken(ctx, {
...baseArgs,
grantType: "client_credentials",
credentialsInBody: false,
custom: {
headers: [
{ name: "content-type", value: "application/json" },
{ name: "Authorization", value: "Custom abc123" },
],
body: [],
},
});
const contentTypes = (sent[0]!.headers ?? []).filter(
(h: { name: string }) => h.name.toLowerCase() === "content-type",
);
expect(contentTypes).toEqual([{ name: "content-type", value: "application/json" }]);
expect(headerValue(sent[0]!, "Authorization")).toBe("Custom abc123");
});
test("sends custom body params in the form-encoded body", async () => {
const { ctx, sent } = createMockContext();
await fetchAccessToken(ctx, {
...baseArgs,
grantType: "password",
params: [{ name: "username", value: "alice" }],
custom: {
headers: [],
body: [{ name: "realm", value: "employees" }],
},
});
expect(sent[0]!.bodyType).toBe("application/x-www-form-urlencoded");
expect(formValue(sent[0]!, "realm")).toBe("employees");
// Generated params survive alongside it
expect(formValue(sent[0]!, "grant_type")).toBe("password");
expect(formValue(sent[0]!, "username")).toBe("alice");
expect(formValue(sent[0]!, "client_id")).toBe("client-123");
});
test("replaces a generated body param of the same name", async () => {
const { ctx, sent } = createMockContext();
await fetchAccessToken(ctx, {
...baseArgs,
grantType: "client_credentials",
custom: {
headers: [],
body: [{ name: "scope", value: "custom-scope" }],
},
});
expect(formNames(sent[0]!).filter((n: string) => n === "scope")).toHaveLength(1);
expect(formValue(sent[0]!, "scope")).toBe("custom-scope");
});
});