Skip to content

Commit bfa416b

Browse files
feat: jwt sub (#6)
* feat: jwt sub * chore: dev version for testing * fix: only add sub when provide * chore: test dev version
1 parent b59bd6e commit bfa416b

File tree

3 files changed

+67
-14
lines changed

3 files changed

+67
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opengovsg/refx-ts-sdk",
3-
"version": "0.0.0-develop-alpha-1763450943",
3+
"version": "0.0.0-develop-alpha-1763535866",
44
"private": false,
55
"repository": "https://github.com/opengovsg/refer-ts-sdk",
66
"main": "./index.js",

src/wrapper/ReferralExchangeJwtClient.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export declare namespace ReferralExchangeJwtClient {
1010
export interface Options extends Omit<ReferralExchangeClient.Options, "fetcher" | "apiKey"> {
1111
privateKey: string;
1212
apiKeyName: string;
13+
subject?: string;
1314
tokenCache?: TokenCacheOptions;
1415
}
1516

@@ -20,8 +21,8 @@ export declare namespace ReferralExchangeJwtClient {
2021

2122
export class ReferralExchangeJwtClient extends ReferralExchangeClient {
2223
constructor(options: ReferralExchangeJwtClient.Options) {
23-
const { privateKey, apiKeyName, tokenCache, ...baseOptions } = options;
24-
const signer = new JwtSigner({ privateKey, issuer: apiKeyName, tokenCache });
24+
const { privateKey, apiKeyName, subject, tokenCache, ...baseOptions } = options;
25+
const signer = new JwtSigner({ privateKey, issuer: apiKeyName, subject, tokenCache });
2526
const fetcher = createJwtFetcher(signer);
2627
super({
2728
...baseOptions,
@@ -33,19 +34,22 @@ export class ReferralExchangeJwtClient extends ReferralExchangeClient {
3334
interface JwtSignerConfig {
3435
privateKey: string;
3536
issuer: string;
37+
subject?: string;
3638
tokenCache?: ReferralExchangeJwtClient.TokenCacheOptions;
3739
}
3840

3941
class JwtSigner {
4042
private readonly privateKey: string;
4143
private readonly issuer: string;
44+
private readonly subject?: string;
4245
private readonly cacheEnabled: boolean;
4346
private readonly refreshBufferSeconds: number;
4447
private cachedToken: { token: string; expiresAtEpochSeconds: number } | undefined;
4548

46-
constructor({ privateKey, issuer, tokenCache }: JwtSignerConfig) {
49+
constructor({ privateKey, issuer, subject, tokenCache }: JwtSignerConfig) {
4750
this.privateKey = privateKey;
4851
this.issuer = issuer;
52+
this.subject = subject;
4953
const cacheEnabled = tokenCache != null;
5054
this.cacheEnabled = cacheEnabled;
5155
this.refreshBufferSeconds = cacheEnabled ? clampRefreshBufferSeconds(tokenCache.refreshBufferSeconds) : 0;
@@ -73,6 +77,7 @@ class JwtSigner {
7377
return createSignedJwt({
7478
privateKey: this.privateKey,
7579
issuer: this.issuer,
80+
subject: this.subject,
7681
});
7782
}
7883
}
@@ -93,24 +98,27 @@ function createJwtFetcher(signer: JwtSigner): FetchFunction {
9398
interface CreateSignedJwtArgs {
9499
privateKey: string;
95100
issuer: string;
101+
subject?: string;
96102
}
97103

98-
function createSignedJwt({ privateKey, issuer }: CreateSignedJwtArgs): {
104+
function createSignedJwt({ privateKey, issuer, subject }: CreateSignedJwtArgs): {
99105
token: string;
100106
expiresAtEpochSeconds: number;
101107
} {
102108
const issuedAt = Math.floor(Date.now() / 1000);
103109
const expiresAtEpochSeconds = issuedAt + JWT_TTL_SECONDS;
110+
const signOptions: jwt.SignOptions = {
111+
algorithm: "ES256",
112+
issuer,
113+
expiresIn: JWT_TTL_SECONDS,
114+
};
115+
116+
// Only add the claim if a value is provided(not null or undefined)
117+
if (subject != null) {
118+
signOptions.subject = subject;
119+
}
104120

105-
const token = jwt.sign(
106-
{},
107-
privateKey,
108-
{
109-
algorithm: "ES256",
110-
issuer,
111-
expiresIn: JWT_TTL_SECONDS,
112-
},
113-
);
121+
const token = jwt.sign({}, privateKey, signOptions);
114122

115123
return {
116124
token,

tests/unit/wrapper/ReferralExchangeJwtClient.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,51 @@ describe("ReferralExchangeJwtClient", () => {
5757
);
5858
});
5959

60+
it("omits subject claim when not provided", async () => {
61+
signMock.mockReturnValue("token-1");
62+
63+
const client = new ReferralExchangeJwtClient({
64+
privateKey: "fake-private-key",
65+
apiKeyName: "issuer",
66+
});
67+
68+
const fetcher = ((client as any)._options.fetcher) as (args: typeof requestArgs) => Promise<unknown>;
69+
70+
await fetcher(requestArgs);
71+
72+
const [, , options] = signMock.mock.calls[0];
73+
expect(options).toEqual(
74+
expect.objectContaining({
75+
issuer: "issuer",
76+
algorithm: "ES256",
77+
}),
78+
);
79+
expect(options).not.toHaveProperty("subject");
80+
});
81+
82+
it("includes subject claim when provided", async () => {
83+
signMock.mockReturnValue("token-1");
84+
85+
const client = new ReferralExchangeJwtClient({
86+
privateKey: "fake-private-key",
87+
apiKeyName: "issuer",
88+
subject: "user-123",
89+
});
90+
91+
const fetcher = ((client as any)._options.fetcher) as (args: typeof requestArgs) => Promise<unknown>;
92+
93+
await fetcher(requestArgs);
94+
95+
expect(signMock).toHaveBeenCalledWith(
96+
{},
97+
"fake-private-key",
98+
expect.objectContaining({
99+
issuer: "issuer",
100+
subject: "user-123",
101+
}),
102+
);
103+
});
104+
60105
it("reuses cached tokens until the refresh buffer elapses", async () => {
61106
signMock.mockReturnValueOnce("token-1").mockReturnValueOnce("token-2");
62107

0 commit comments

Comments
 (0)