Skip to content

Commit ab4e24f

Browse files
authored
Make token refresher init itself lazily (#5106)
* Make token refresher init itself lazily It needs a network connection to do the init, so this would fail if a client tried to do it at startup with no internet, causing the token to just never be refreshed. This just changes the API (compatibly) to do the init lazily. The promise is kept is retain backwards compat, it can be removed later. * Make deviceId protected * Fix tests
1 parent 2218ec4 commit ab4e24f

File tree

2 files changed

+72
-51
lines changed

2 files changed

+72
-51
lines changed

spec/unit/oidc/tokenRefresher.spec.ts

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ limitations under the License.
2121
import fetchMock from "fetch-mock-jest";
2222

2323
import { OidcTokenRefresher, TokenRefreshLogoutError } from "../../../src";
24-
import { logger } from "../../../src/logger";
2524
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
2625

2726
describe("OidcTokenRefresher", () => {
@@ -78,51 +77,49 @@ describe("OidcTokenRefresher", () => {
7877
fetchMock.resetBehavior();
7978
});
8079

81-
it("throws when oidc client cannot be initialised", async () => {
82-
jest.spyOn(logger, "error");
83-
fetchMock.get(
84-
`${config.issuer}.well-known/openid-configuration`,
85-
{
86-
ok: false,
87-
status: 404,
88-
},
89-
{ overwriteRoutes: true },
90-
);
91-
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
92-
await expect(refresher.oidcClientReady).rejects.toThrow();
93-
expect(logger.error).toHaveBeenCalledWith(
94-
"Failed to initialise OIDC client.",
95-
// error from OidcClient
96-
expect.any(Error),
97-
);
98-
});
99-
100-
it("initialises oidc client", async () => {
101-
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
102-
await refresher.oidcClientReady;
103-
104-
// @ts-ignore peek at private property to see we initialised the client correctly
105-
expect(refresher.oidcClient.settings).toEqual(
106-
expect.objectContaining({
107-
client_id: clientId,
108-
redirect_uri: redirectUri,
109-
authority: authConfig.issuer,
110-
scope,
111-
}),
112-
);
113-
});
114-
11580
describe("doRefreshAccessToken()", () => {
11681
it("should throw when oidcClient has not been initialised", async () => {
82+
fetchMock.get(
83+
`${config.issuer}.well-known/openid-configuration`,
84+
{
85+
ok: false,
86+
status: 404,
87+
},
88+
{ overwriteRoutes: true },
89+
);
90+
91+
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
92+
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client.");
93+
});
94+
95+
it("should retry initialisation", async () => {
96+
fetchMock.get(
97+
`${config.issuer}.well-known/openid-configuration`,
98+
{
99+
ok: false,
100+
status: 404,
101+
},
102+
{ overwriteRoutes: true },
103+
);
104+
117105
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
118-
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
119-
"Cannot get new token before OIDC client is initialised.",
106+
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client.");
107+
108+
// put the successful mock back
109+
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config, { overwriteRoutes: true });
110+
111+
const result = await refresher.doRefreshAccessToken("token");
112+
113+
expect(result).toEqual(
114+
expect.objectContaining({
115+
accessToken: "new-access-token",
116+
refreshToken: "new-refresh-token",
117+
}),
120118
);
121119
});
122120

123121
it("should refresh the tokens", async () => {
124122
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
125-
await refresher.oidcClientReady;
126123

127124
const result = await refresher.doRefreshAccessToken("refresh-token");
128125

@@ -140,13 +137,12 @@ describe("OidcTokenRefresher", () => {
140137

141138
it("should persist the new tokens", async () => {
142139
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
143-
await refresher.oidcClientReady;
144140
// spy on our stub
145-
jest.spyOn(refresher, "persistTokens");
141+
jest.spyOn(refresher as any, "persistTokens");
146142

147143
await refresher.doRefreshAccessToken("refresh-token");
148144

149-
expect(refresher.persistTokens).toHaveBeenCalledWith(
145+
expect((refresher as any).persistTokens).toHaveBeenCalledWith(
150146
expect.objectContaining({
151147
accessToken: "new-access-token",
152148
refreshToken: "new-refresh-token",

src/oidc/tokenRefresher.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,62 @@ import { logger } from "../logger.ts";
3030
*/
3131
export class OidcTokenRefresher {
3232
/**
33-
* Promise which will complete once the OidcClient has been initialised
34-
* and is ready to start refreshing tokens.
35-
*
36-
* Will reject if the client initialisation fails.
33+
* This is now just a resolved promise and will be removed in a future version.
34+
* Initialisation is done lazily at token refresh time.
35+
* @deprecated Consumers no longer need to wait for this promise.
3736
*/
3837
public readonly oidcClientReady!: Promise<void>;
38+
39+
// If there is a initialisation attempt in progress, we keep track of it here.
40+
private initPromise?: Promise<void>;
41+
3942
private oidcClient!: OidcClient;
4043
private inflightRefreshRequest?: Promise<AccessTokens>;
4144

4245
public constructor(
4346
/**
4447
* The OIDC issuer as returned by the /auth_issuer API
4548
*/
46-
issuer: string,
49+
private issuer: string,
4750
/**
4851
* id of this client as registered with the OP
4952
*/
50-
clientId: string,
53+
private clientId: string,
5154
/**
5255
* redirectUri as registered with OP
5356
*/
54-
redirectUri: string,
57+
private redirectUri: string,
5558
/**
5659
* Device ID of current session
5760
*/
58-
deviceId: string,
61+
protected deviceId: string,
5962
/**
6063
* idTokenClaims as returned from authorization grant
6164
* used to validate tokens
6265
*/
6366
private readonly idTokenClaims: IdTokenClaims,
6467
) {
65-
this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri);
68+
this.oidcClientReady = Promise.resolve();
69+
}
70+
71+
/**
72+
* Ensures that the client is initialised.
73+
* @returns Promise that resolves when initialisation is complete
74+
* @throws if initialisation fails
75+
*/
76+
private async ensureInit(): Promise<void> {
77+
if (!this.oidcClient) {
78+
if (this.initPromise) {
79+
return this.initPromise;
80+
}
81+
82+
this.initPromise = this.initialiseOidcClient(this.issuer, this.clientId, this.deviceId, this.redirectUri);
83+
try {
84+
await this.initPromise;
85+
} finally {
86+
this.initPromise = undefined;
87+
}
88+
}
6689
}
6790

6891
private async initialiseOidcClient(
@@ -98,6 +121,8 @@ export class OidcTokenRefresher {
98121
* @throws when token refresh fails
99122
*/
100123
public async doRefreshAccessToken(refreshToken: string): Promise<AccessTokens> {
124+
await this.ensureInit();
125+
101126
if (!this.inflightRefreshRequest) {
102127
this.inflightRefreshRequest = this.getNewTokens(refreshToken);
103128
}
@@ -123,7 +148,7 @@ export class OidcTokenRefresher {
123148
* @param tokens.accessToken - new access token
124149
* @param tokens.refreshToken - OPTIONAL new refresh token
125150
*/
126-
public async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise<void> {
151+
protected async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise<void> {
127152
// NOOP
128153
}
129154

0 commit comments

Comments
 (0)