Skip to content

Commit 38244b8

Browse files
committed
Merge remote-tracking branch 'SightStudio/feat/oauth-client-auth-methods' into ochafik/auth-merge-531-552
2 parents b803121 + ab2f890 commit 38244b8

File tree

2 files changed

+386
-20
lines changed

2 files changed

+386
-20
lines changed

src/client/auth.test.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,4 +922,232 @@ describe("OAuth Authorization", () => {
922922
);
923923
});
924924
});
925+
926+
describe("exchangeAuthorization with multiple client authentication methods", () => {
927+
const validTokens = {
928+
access_token: "access123",
929+
token_type: "Bearer",
930+
expires_in: 3600,
931+
refresh_token: "refresh123",
932+
};
933+
934+
const validClientInfo = {
935+
client_id: "client123",
936+
client_secret: "secret123",
937+
redirect_uris: ["http://localhost:3000/callback"],
938+
client_name: "Test Client",
939+
};
940+
941+
const metadataWithBasicOnly = {
942+
issuer: "https://auth.example.com",
943+
authorization_endpoint: "https://auth.example.com/auth",
944+
token_endpoint: "https://auth.example.com/token",
945+
response_types_supported: ["code"],
946+
code_challenge_methods_supported: ["S256"],
947+
token_endpoint_auth_methods_supported: ["client_secret_basic"],
948+
};
949+
950+
const metadataWithPostOnly = {
951+
...metadataWithBasicOnly,
952+
token_endpoint_auth_methods_supported: ["client_secret_post"],
953+
};
954+
955+
const metadataWithNoneOnly = {
956+
...metadataWithBasicOnly,
957+
token_endpoint_auth_methods_supported: ["none"],
958+
};
959+
960+
it("uses HTTP Basic authentication when client_secret_basic is supported", async () => {
961+
mockFetch.mockResolvedValueOnce({
962+
ok: true,
963+
status: 200,
964+
json: async () => validTokens,
965+
});
966+
967+
const tokens = await exchangeAuthorization("https://auth.example.com", {
968+
metadata: metadataWithBasicOnly,
969+
clientInformation: validClientInfo,
970+
authorizationCode: "code123",
971+
codeVerifier: "verifier123",
972+
redirectUri: "http://localhost:3000/callback",
973+
});
974+
975+
expect(tokens).toEqual(validTokens);
976+
const request = mockFetch.mock.calls[0][1];
977+
978+
// Check Authorization header
979+
const authHeader = request.headers["Authorization"];
980+
const expected = "Basic " + btoa("client123:secret123");
981+
expect(authHeader).toBe(expected);
982+
983+
const body = request.body as URLSearchParams;
984+
expect(body.get("client_id")).toBeNull(); // should not be in body
985+
expect(body.get("client_secret")).toBeNull(); // should not be in body
986+
});
987+
988+
it("includes credentials in request body when client_secret_post is supported", async () => {
989+
mockFetch.mockResolvedValueOnce({
990+
ok: true,
991+
status: 200,
992+
json: async () => validTokens,
993+
});
994+
995+
const tokens = await exchangeAuthorization("https://auth.example.com", {
996+
metadata: metadataWithPostOnly,
997+
clientInformation: validClientInfo,
998+
authorizationCode: "code123",
999+
codeVerifier: "verifier123",
1000+
redirectUri: "http://localhost:3000/callback",
1001+
});
1002+
1003+
expect(tokens).toEqual(validTokens);
1004+
const request = mockFetch.mock.calls[0][1];
1005+
1006+
// Check no Authorization header
1007+
expect(request.headers["Authorization"]).toBeUndefined();
1008+
1009+
const body = request.body as URLSearchParams;
1010+
expect(body.get("client_id")).toBe("client123");
1011+
expect(body.get("client_secret")).toBe("secret123");
1012+
});
1013+
1014+
it("uses public client authentication when none method is specified", async () => {
1015+
mockFetch.mockResolvedValueOnce({
1016+
ok: true,
1017+
status: 200,
1018+
json: async () => validTokens,
1019+
});
1020+
1021+
const clientInfoWithoutSecret = {
1022+
client_id: "client123",
1023+
redirect_uris: ["http://localhost:3000/callback"],
1024+
client_name: "Test Client",
1025+
};
1026+
1027+
const tokens = await exchangeAuthorization("https://auth.example.com", {
1028+
metadata: metadataWithNoneOnly,
1029+
clientInformation: clientInfoWithoutSecret,
1030+
authorizationCode: "code123",
1031+
codeVerifier: "verifier123",
1032+
redirectUri: "http://localhost:3000/callback",
1033+
});
1034+
1035+
expect(tokens).toEqual(validTokens);
1036+
const request = mockFetch.mock.calls[0][1];
1037+
1038+
// Check no Authorization header
1039+
expect(request.headers["Authorization"]).toBeUndefined();
1040+
1041+
const body = request.body as URLSearchParams;
1042+
expect(body.get("client_id")).toBe("client123");
1043+
expect(body.get("client_secret")).toBeNull();
1044+
});
1045+
1046+
it("defaults to client_secret_post when no auth methods specified", async () => {
1047+
mockFetch.mockResolvedValueOnce({
1048+
ok: true,
1049+
status: 200,
1050+
json: async () => validTokens,
1051+
});
1052+
1053+
const tokens = await exchangeAuthorization("https://auth.example.com", {
1054+
clientInformation: validClientInfo,
1055+
authorizationCode: "code123",
1056+
codeVerifier: "verifier123",
1057+
redirectUri: "http://localhost:3000/callback",
1058+
});
1059+
1060+
expect(tokens).toEqual(validTokens);
1061+
const request = mockFetch.mock.calls[0][1];
1062+
1063+
// Check no Authorization header
1064+
expect(request.headers["Authorization"]).toBeUndefined();
1065+
1066+
const body = request.body as URLSearchParams;
1067+
expect(body.get("client_id")).toBe("client123");
1068+
expect(body.get("client_secret")).toBe("secret123");
1069+
});
1070+
});
1071+
1072+
describe("refreshAuthorization with multiple client authentication methods", () => {
1073+
const validTokens = {
1074+
access_token: "newaccess123",
1075+
token_type: "Bearer",
1076+
expires_in: 3600,
1077+
refresh_token: "newrefresh123",
1078+
};
1079+
1080+
const validClientInfo = {
1081+
client_id: "client123",
1082+
client_secret: "secret123",
1083+
redirect_uris: ["http://localhost:3000/callback"],
1084+
client_name: "Test Client",
1085+
};
1086+
1087+
const metadataWithBasicOnly = {
1088+
issuer: "https://auth.example.com",
1089+
authorization_endpoint: "https://auth.example.com/auth",
1090+
token_endpoint: "https://auth.example.com/token",
1091+
response_types_supported: ["code"],
1092+
token_endpoint_auth_methods_supported: ["client_secret_basic"],
1093+
};
1094+
1095+
const metadataWithPostOnly = {
1096+
...metadataWithBasicOnly,
1097+
token_endpoint_auth_methods_supported: ["client_secret_post"],
1098+
};
1099+
1100+
it("uses client_secret_basic for refresh token", async () => {
1101+
mockFetch.mockResolvedValueOnce({
1102+
ok: true,
1103+
status: 200,
1104+
json: async () => validTokens,
1105+
});
1106+
1107+
const tokens = await refreshAuthorization("https://auth.example.com", {
1108+
metadata: metadataWithBasicOnly,
1109+
clientInformation: validClientInfo,
1110+
refreshToken: "refresh123",
1111+
});
1112+
1113+
expect(tokens).toEqual(validTokens);
1114+
const request = mockFetch.mock.calls[0][1];
1115+
1116+
// Check Authorization header
1117+
const authHeader = request.headers["Authorization"];
1118+
const expected = "Basic " + btoa("client123:secret123");
1119+
expect(authHeader).toBe(expected);
1120+
1121+
const body = request.body as URLSearchParams;
1122+
expect(body.get("client_id")).toBeNull(); // should not be in body
1123+
expect(body.get("client_secret")).toBeNull(); // should not be in body
1124+
expect(body.get("refresh_token")).toBe("refresh123");
1125+
});
1126+
1127+
it("uses client_secret_post for refresh token", async () => {
1128+
mockFetch.mockResolvedValueOnce({
1129+
ok: true,
1130+
status: 200,
1131+
json: async () => validTokens,
1132+
});
1133+
1134+
const tokens = await refreshAuthorization("https://auth.example.com", {
1135+
metadata: metadataWithPostOnly,
1136+
clientInformation: validClientInfo,
1137+
refreshToken: "refresh123",
1138+
});
1139+
1140+
expect(tokens).toEqual(validTokens);
1141+
const request = mockFetch.mock.calls[0][1];
1142+
1143+
// Check no Authorization header
1144+
expect(request.headers["Authorization"]).toBeUndefined();
1145+
1146+
const body = request.body as URLSearchParams;
1147+
expect(body.get("client_id")).toBe("client123");
1148+
expect(body.get("client_secret")).toBe("secret123");
1149+
expect(body.get("refresh_token")).toBe("refresh123");
1150+
});
1151+
});
1152+
9251153
});

0 commit comments

Comments
 (0)