Skip to content

Commit 8b4ee92

Browse files
author
Brice Broussolle
committed
Add static or dynamic custom header for each client calls.
1 parent b4fe907 commit 8b4ee92

9 files changed

Lines changed: 239 additions & 5 deletions

docs/oidc-client-ts.api.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ export type CreateSignoutRequestArgs = Omit<SignoutRequestArgs, "url" | "state_d
9595
state?: unknown;
9696
};
9797

98+
// @public (undocumented)
99+
export type CustomHeader = string | (() => string);
100+
98101
// @public
99102
export class ErrorResponse extends Error {
100103
constructor(args: {
@@ -339,6 +342,7 @@ export interface OidcClientSettings {
339342
client_secret?: string;
340343
// @deprecated (undocumented)
341344
clockSkewInSeconds?: number;
345+
customHeaders?: Record<string, CustomHeader>;
342346
disablePKCE?: boolean;
343347
display?: string;
344348
extraQueryParams?: Record<string, string | number | boolean>;
@@ -374,7 +378,7 @@ export interface OidcClientSettings {
374378

375379
// @public
376380
export class OidcClientSettingsStore {
377-
constructor({ authority, metadataUrl, metadata, signingKeys, metadataSeed, client_id, client_secret, response_type, scope, redirect_uri, post_logout_redirect_uri, client_authentication, prompt, display, max_age, ui_locales, acr_values, resource, response_mode, filterProtocolClaims, loadUserInfo, staleStateAgeInSeconds, clockSkewInSeconds, userInfoJwtIssuer, mergeClaims, disablePKCE, stateStore, refreshTokenCredentials, revokeTokenAdditionalContentTypes, fetchRequestCredentials, refreshTokenAllowedScope, extraQueryParams, extraTokenParams, }: OidcClientSettings);
381+
constructor({ authority, metadataUrl, metadata, signingKeys, metadataSeed, client_id, client_secret, response_type, scope, redirect_uri, post_logout_redirect_uri, client_authentication, prompt, display, max_age, ui_locales, acr_values, resource, response_mode, filterProtocolClaims, loadUserInfo, staleStateAgeInSeconds, clockSkewInSeconds, userInfoJwtIssuer, mergeClaims, disablePKCE, stateStore, refreshTokenCredentials, revokeTokenAdditionalContentTypes, fetchRequestCredentials, refreshTokenAllowedScope, extraQueryParams, extraTokenParams, customHeaders, }: OidcClientSettings);
378382
// (undocumented)
379383
readonly acr_values: string | undefined;
380384
// (undocumented)
@@ -388,6 +392,8 @@ export class OidcClientSettingsStore {
388392
// (undocumented)
389393
readonly clockSkewInSeconds: number;
390394
// (undocumented)
395+
readonly customHeaders: Record<string, CustomHeader>;
396+
// (undocumented)
391397
readonly disablePKCE: boolean;
392398
// (undocumented)
393399
readonly display: string | undefined;

src/JsonService.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,30 @@ import { mocked } from "jest-mock";
88

99
describe("JsonService", () => {
1010
let subject: JsonService;
11+
let customStaticHeaderSubject: JsonService;
12+
let customDynamicHeaderSubject: JsonService;
13+
14+
const staticCustomHeaders = {
15+
"Custom-Header-1": "this-is-header-1",
16+
"Custom-Header-2": "this-is-header-2",
17+
"acCept" : "application/fake",
18+
"AuthoriZation" : "not good",
19+
"Content-Type": "application/fail",
20+
};
21+
const dynamicCustomHeaders = {
22+
"Custom-Header-1": () => "my-name-is-header-1",
23+
"Custom-Header-2": () => {
24+
return "my-name-is-header-2";
25+
},
26+
"acCept" : () => "nothing",
27+
"AuthoriZation" : () => "not good",
28+
"Content-Type": "application/fail",
29+
};
1130

1231
beforeEach(() =>{
1332
subject = new JsonService();
33+
customStaticHeaderSubject = new JsonService(undefined, null, staticCustomHeaders);
34+
customDynamicHeaderSubject = new JsonService(undefined, null, dynamicCustomHeaders);
1435
});
1536

1637
describe("getJson", () => {
@@ -28,6 +49,42 @@ describe("JsonService", () => {
2849
);
2950
});
3051

52+
it("should make GET request to url with static custom headers", async () => {
53+
// act
54+
await expect(customStaticHeaderSubject.getJson("http://test")).rejects.toThrow();
55+
56+
// assert
57+
expect(fetch).toBeCalledWith(
58+
"http://test",
59+
expect.objectContaining({
60+
headers: {
61+
Accept: "application/json",
62+
"Custom-Header-1": "this-is-header-1",
63+
"Custom-Header-2": "this-is-header-2",
64+
},
65+
method: "GET",
66+
}),
67+
);
68+
});
69+
70+
it("should make GET request to url with dynamic custom headers", async () => {
71+
// act
72+
await expect(customDynamicHeaderSubject.getJson("http://test")).rejects.toThrow();
73+
74+
// assert
75+
expect(fetch).toBeCalledWith(
76+
"http://test",
77+
expect.objectContaining({
78+
headers: {
79+
Accept: "application/json",
80+
"Custom-Header-1": "my-name-is-header-1",
81+
"Custom-Header-2": "my-name-is-header-2",
82+
},
83+
method: "GET",
84+
}),
85+
);
86+
});
87+
3188
it("should set token as authorization header", async () => {
3289
// act
3390
await expect(subject.getJson("http://test", { token: "token" })).rejects.toThrow();
@@ -42,6 +99,44 @@ describe("JsonService", () => {
4299
);
43100
});
44101

102+
it("should set token as authorization header with static custom headers", async () => {
103+
// act
104+
await expect(customStaticHeaderSubject.getJson("http://test", { token: "token" })).rejects.toThrow();
105+
106+
// assert
107+
expect(fetch).toBeCalledWith(
108+
"http://test",
109+
expect.objectContaining({
110+
headers: {
111+
Accept: "application/json",
112+
Authorization: "Bearer token",
113+
"Custom-Header-1": "this-is-header-1",
114+
"Custom-Header-2": "this-is-header-2",
115+
},
116+
method: "GET",
117+
}),
118+
);
119+
});
120+
121+
it("should set token as authorization header with dynamic custom headers", async () => {
122+
// act
123+
await expect(customDynamicHeaderSubject.getJson("http://test", { token: "token" })).rejects.toThrow();
124+
125+
// assert
126+
expect(fetch).toBeCalledWith(
127+
"http://test",
128+
expect.objectContaining({
129+
headers: {
130+
Accept: "application/json",
131+
Authorization: "Bearer token",
132+
"Custom-Header-1": "my-name-is-header-1",
133+
"Custom-Header-2": "my-name-is-header-2",
134+
},
135+
method: "GET",
136+
}),
137+
);
138+
});
139+
45140
it("should fulfill promise when http response is 200", async () => {
46141
// arrange
47142
const json = { foo: 1, bar: "test" };
@@ -185,6 +280,46 @@ describe("JsonService", () => {
185280
);
186281
});
187282

283+
it("should make POST request to url with custom static headers", async () => {
284+
// act
285+
await expect(customStaticHeaderSubject.postForm("http://test", { body: new URLSearchParams("a=b") })).rejects.toThrow();
286+
287+
// assert
288+
expect(fetch).toBeCalledWith(
289+
"http://test",
290+
expect.objectContaining({
291+
headers: {
292+
Accept: "application/json",
293+
"Content-Type": "application/x-www-form-urlencoded",
294+
"Custom-Header-1": "this-is-header-1",
295+
"Custom-Header-2": "this-is-header-2",
296+
},
297+
method: "POST",
298+
body: new URLSearchParams(),
299+
}),
300+
);
301+
});
302+
303+
it("should make POST request to url with custom dynamic headers", async () => {
304+
// act
305+
await expect(customDynamicHeaderSubject.postForm("http://test", { body: new URLSearchParams("a=b") })).rejects.toThrow();
306+
307+
// assert
308+
expect(fetch).toBeCalledWith(
309+
"http://test",
310+
expect.objectContaining({
311+
headers: {
312+
Accept: "application/json",
313+
"Content-Type": "application/x-www-form-urlencoded",
314+
"Custom-Header-1": "my-name-is-header-1",
315+
"Custom-Header-2": "my-name-is-header-2",
316+
},
317+
method: "POST",
318+
body: new URLSearchParams(),
319+
}),
320+
);
321+
});
322+
188323
it("should set basicAuth as authorization header", async () => {
189324
// act
190325
await expect(subject.postForm("http://test", { body: new URLSearchParams("payload=dummy"), basicAuth: "basicAuth" })).rejects.toThrow();

src/JsonService.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import { Logger } from "./utils";
99
*/
1010
export type JwtHandler = (text: string) => Promise<Record<string, unknown>>;
1111

12+
/**
13+
* @public
14+
*/
15+
export type CustomHeader = string | (() => string);
16+
1217
/**
1318
* @internal
1419
*/
@@ -27,6 +32,33 @@ export interface PostFormOpts {
2732
initCredentials?: "same-origin" | "include" | "omit";
2833
}
2934

35+
/**
36+
* @internal
37+
*/
38+
function appendCustomHeaders(
39+
headers: Record<string, string>,
40+
customs: Record<string, CustomHeader>,
41+
): void {
42+
const customKeys = Object.keys(customs);
43+
const protectedHeaders = [
44+
"authorization",
45+
"accept",
46+
"content-type",
47+
];
48+
if (customKeys.length === 0) {
49+
return;
50+
}
51+
customKeys.forEach((headerName) => {
52+
if (protectedHeaders.includes(headerName.toLocaleLowerCase())) {
53+
return;
54+
}
55+
const content = (typeof customs[headerName] === "function") ?
56+
(customs[headerName] as ()=>string)() :
57+
customs[headerName];
58+
headers[headerName] = content as string;
59+
});
60+
}
61+
3062
/**
3163
* @internal
3264
*/
@@ -38,6 +70,7 @@ export class JsonService {
3870
public constructor(
3971
additionalContentTypes: string[] = [],
4072
private _jwtHandler: JwtHandler | null = null,
73+
private _customHeaders: Record<string, CustomHeader> = {},
4174
) {
4275
this._contentTypes.push(...additionalContentTypes, "application/json");
4376
if (_jwtHandler) {
@@ -85,6 +118,8 @@ export class JsonService {
85118
headers["Authorization"] = "Bearer " + token;
86119
}
87120

121+
appendCustomHeaders(headers, this._customHeaders);
122+
88123
let response: Response;
89124
try {
90125
logger.debug("url:", url);
@@ -136,6 +171,7 @@ export class JsonService {
136171
if (basicAuth !== undefined) {
137172
headers["Authorization"] = "Basic " + basicAuth;
138173
}
174+
appendCustomHeaders(headers, this._customHeaders);
139175

140176
let response: Response;
141177
try {

src/MetadataService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { OidcMetadata } from "./OidcMetadata";
1111
*/
1212
export class MetadataService {
1313
private readonly _logger = new Logger("MetadataService");
14-
private readonly _jsonService = new JsonService(["application/jwk-set+json"]);
14+
private readonly _jsonService;
1515

1616
// cache
1717
private _metadataUrl: string;
@@ -21,7 +21,11 @@ export class MetadataService {
2121

2222
public constructor(private readonly _settings: OidcClientSettingsStore) {
2323
this._metadataUrl = this._settings.metadataUrl;
24-
24+
this._jsonService = new JsonService(
25+
["application/jwk-set+json"],
26+
null,
27+
this._settings.customHeaders,
28+
);
2529
if (this._settings.signingKeys) {
2630
this._logger.debug("using signingKeys from settings");
2731
this._signingKeys = this._settings.signingKeys;

src/OidcClientSettings.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,4 +526,36 @@ describe("OidcClientSettings", () => {
526526
expect(subject.extraTokenParams).toEqual({ "resourceServer": "abc" });
527527
});
528528
});
529+
530+
describe("customHeaders", () => {
531+
532+
it("should use default value", () => {
533+
// act
534+
const subject = new OidcClientSettingsStore({
535+
authority: "authority",
536+
client_id: "client",
537+
redirect_uri: "redirect",
538+
});
539+
540+
// assert
541+
expect(subject.customHeaders).toEqual({});
542+
});
543+
544+
it("should return value from initial settings", () => {
545+
// act
546+
const customHeaders = {
547+
"Header-1": "this-is-a-test",
548+
"Header-3": () => "dynamic header",
549+
};
550+
const subject = new OidcClientSettingsStore({
551+
authority: "authority",
552+
client_id: "client",
553+
redirect_uri: "redirect",
554+
customHeaders: customHeaders,
555+
});
556+
557+
// assert
558+
expect(subject.customHeaders).toEqual(customHeaders);
559+
});
560+
});
529561
});

src/OidcClientSettings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { WebStorageStateStore } from "./WebStorageStateStore";
55
import type { OidcMetadata } from "./OidcMetadata";
66
import type { StateStore } from "./StateStore";
77
import { InMemoryWebStorage } from "./InMemoryWebStorage";
8+
import type { CustomHeader } from "./JsonService";
89

910
const DefaultResponseType = "code";
1011
const DefaultScope = "openid";
@@ -130,6 +131,11 @@ export interface OidcClientSettings {
130131
* Only scopes in this list will be passed in the token refresh request.
131132
*/
132133
refreshTokenAllowedScope?: string | undefined;
134+
135+
/**
136+
* Set additional custom headers to be passed to the client
137+
*/
138+
customHeaders?: Record<string, CustomHeader>;
133139
}
134140

135141
/**
@@ -182,6 +188,9 @@ export class OidcClientSettingsStore {
182188
public readonly fetchRequestCredentials: RequestCredentials;
183189
public readonly refreshTokenAllowedScope: string | undefined;
184190
public readonly disablePKCE: boolean;
191+
192+
// headers
193+
public readonly customHeaders: Record<string, CustomHeader>;
185194

186195
public constructor({
187196
// metadata related
@@ -209,6 +218,8 @@ export class OidcClientSettingsStore {
209218
// extra query params
210219
extraQueryParams = {},
211220
extraTokenParams = {},
221+
// custom headers
222+
customHeaders = {},
212223
}: OidcClientSettings) {
213224

214225
this.authority = authority;
@@ -272,5 +283,6 @@ export class OidcClientSettingsStore {
272283

273284
this.extraQueryParams = extraQueryParams;
274285
this.extraTokenParams = extraTokenParams;
286+
this.customHeaders = customHeaders;
275287
}
276288
}

src/TokenClient.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ export class TokenClient {
6666
private readonly _settings: OidcClientSettingsStore,
6767
private readonly _metadataService: MetadataService,
6868
) {
69-
this._jsonService = new JsonService(this._settings.revokeTokenAdditionalContentTypes);
69+
this._jsonService = new JsonService(
70+
this._settings.revokeTokenAdditionalContentTypes,
71+
null,
72+
this._settings.customHeaders,
73+
);
7074
}
7175

7276
public async exchangeCode({

0 commit comments

Comments
 (0)