Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions packages/core/sdk/src/oauth-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ describe("exchangeAuthorizationCode", () => {
});
const call = (yield* calls)[0]!;
expect(call.headers["content-type"]).toBe("application/json");
expect(call.headers["authorization"]).toBe("Basic Y2lkOmMlMkRzZWNyZXQ=");
expect(call.headers["authorization"]).toBe("Basic Y2lkOmMtc2VjcmV0");
expect(call.jsonBody).toEqual({
grant_type: "authorization_code",
code: "abc",
Expand Down Expand Up @@ -818,7 +818,7 @@ describe("exchangeAuthorizationCode", () => {
clientAuth: "basic",
});
const call = (yield* calls)[0]!;
const expected = `Basic ${Buffer.from("cid:c%2Dsecret").toString("base64")}`;
const expected = `Basic ${Buffer.from("cid:c-secret").toString("base64")}`;
expect(call.headers["authorization"]).toBe(expected);
expect(call.body.has("client_id")).toBe(false);
expect(call.body.has("client_secret")).toBe(false);
Expand Down Expand Up @@ -1312,6 +1312,26 @@ describe("exchangeAuthorizationCode", () => {
});

describe("exchangeClientCredentials", () => {
it.effect("uses literal HTTP Basic credentials when clientAuth=basic", () =>
withTokenEndpoint(tokenResponse(validRefreshBody), ({ tokenUrl, calls }) =>
Effect.gen(function* () {
yield* exchangeClientCredentials({
tokenUrl,
clientId: "client_id-with-punctuation",
clientSecret: "client_secret-with-punctuation",
clientAuth: "basic",
});
const call = (yield* calls)[0]!;
const expected = `Basic ${Buffer.from(
"client_id-with-punctuation:client_secret-with-punctuation",
).toString("base64")}`;
expect(call.headers["authorization"]).toBe(expected);
expect(call.body.has("client_id")).toBe(false);
expect(call.body.has("client_secret")).toBe(false);
}),
),
);

it.effect("routes token grant requests through the injected fetch", () =>
withTokenEndpoint(tokenResponse(validRefreshBody), ({ tokenUrl }) =>
Effect.gen(function* () {
Expand Down
18 changes: 12 additions & 6 deletions packages/core/sdk/src/oauth-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,10 +850,13 @@ export type ClientAuthMethod = TokenEndpointAuthMethod;
* method our DCR registers (`token_endpoint_auth_method: client_secret_post`)
* and the one every confidential client in the v2 model uses. EXPLICIT and
* documented rather than a hidden inline `?? "body"`: callers that need
* `client_secret_basic` pass `clientAuth: "basic"`. Providers that reject the
* RFC form encoding can explicitly pass `clientAuth: "basic_raw"`. For PUBLIC
* clients (no secret) the method is irrelevant — `pickClientAuth` returns
* `None()`.
* `client_secret_basic` pass `clientAuth: "basic"` or `"basic_raw"`. Both send
* the interoperable HTTP Basic representation (Base64 of the literal UTF-8
* `client_id:client_secret` pair). oauth4webapi's RFC 6749 form-encoding of
* each component before Base64 is not used: providers such as Aikido compare
* the decoded username and password literally, so `_` becoming `%5F` is a
* rejected credential. For PUBLIC clients (no secret) the method is
* irrelevant — `pickClientAuth` returns `None()`.
*/
export const DEFAULT_CLIENT_AUTH_METHOD: ClientAuthMethod = "body";

Expand Down Expand Up @@ -936,8 +939,11 @@ const pickClientAuth = (
method: ClientAuthMethod,
): oauth.ClientAuth => {
if (!clientSecret) return oauth.None();
if (method === "basic") return oauth.ClientSecretBasic(clientSecret);
if (method === "basic_raw") return rawClientSecretBasic(clientSecret);
// `"basic"` and `"basic_raw"` are the same wire format on this fork: the
// interoperable literal pair, not oauth4webapi's RFC-strict form-encoding.
// Stored Aikido apps use `"basic"`; upstream later split a `"basic_raw"`
// alias. Collapsing them keeps refresh and re-mint working for those rows.
if (method === "basic" || method === "basic_raw") return rawClientSecretBasic(clientSecret);
return oauth.ClientSecretPost(clientSecret);
};

Expand Down