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
This commit is contained in:
@@ -21,7 +21,6 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { OidcTokenRefresher, TokenRefreshLogoutError } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
describe("OidcTokenRefresher", () => {
|
||||
@@ -78,51 +77,49 @@ describe("OidcTokenRefresher", () => {
|
||||
fetchMock.resetBehavior();
|
||||
});
|
||||
|
||||
it("throws when oidc client cannot be initialised", async () => {
|
||||
jest.spyOn(logger, "error");
|
||||
fetchMock.get(
|
||||
`${config.issuer}.well-known/openid-configuration`,
|
||||
{
|
||||
ok: false,
|
||||
status: 404,
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.oidcClientReady).rejects.toThrow();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Failed to initialise OIDC client.",
|
||||
// error from OidcClient
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
it("initialises oidc client", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
// @ts-ignore peek at private property to see we initialised the client correctly
|
||||
expect(refresher.oidcClient.settings).toEqual(
|
||||
expect.objectContaining({
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
authority: authConfig.issuer,
|
||||
scope,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("doRefreshAccessToken()", () => {
|
||||
it("should throw when oidcClient has not been initialised", async () => {
|
||||
fetchMock.get(
|
||||
`${config.issuer}.well-known/openid-configuration`,
|
||||
{
|
||||
ok: false,
|
||||
status: 404,
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
|
||||
"Cannot get new token before OIDC client is initialised.",
|
||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client.");
|
||||
});
|
||||
|
||||
it("should retry initialisation", async () => {
|
||||
fetchMock.get(
|
||||
`${config.issuer}.well-known/openid-configuration`,
|
||||
{
|
||||
ok: false,
|
||||
status: 404,
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client.");
|
||||
|
||||
// put the successful mock back
|
||||
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config, { overwriteRoutes: true });
|
||||
|
||||
const result = await refresher.doRefreshAccessToken("token");
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
accessToken: "new-access-token",
|
||||
refreshToken: "new-refresh-token",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should refresh the tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||
|
||||
@@ -140,13 +137,12 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
it("should persist the new tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// spy on our stub
|
||||
jest.spyOn(refresher, "persistTokens");
|
||||
jest.spyOn(refresher as any, "persistTokens");
|
||||
|
||||
await refresher.doRefreshAccessToken("refresh-token");
|
||||
|
||||
expect(refresher.persistTokens).toHaveBeenCalledWith(
|
||||
expect((refresher as any).persistTokens).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accessToken: "new-access-token",
|
||||
refreshToken: "new-refresh-token",
|
||||
|
||||
+35
-10
@@ -30,12 +30,15 @@ import { logger } from "../logger.ts";
|
||||
*/
|
||||
export class OidcTokenRefresher {
|
||||
/**
|
||||
* Promise which will complete once the OidcClient has been initialised
|
||||
* and is ready to start refreshing tokens.
|
||||
*
|
||||
* Will reject if the client initialisation fails.
|
||||
* This is now just a resolved promise and will be removed in a future version.
|
||||
* Initialisation is done lazily at token refresh time.
|
||||
* @deprecated Consumers no longer need to wait for this promise.
|
||||
*/
|
||||
public readonly oidcClientReady!: Promise<void>;
|
||||
|
||||
// If there is a initialisation attempt in progress, we keep track of it here.
|
||||
private initPromise?: Promise<void>;
|
||||
|
||||
private oidcClient!: OidcClient;
|
||||
private inflightRefreshRequest?: Promise<AccessTokens>;
|
||||
|
||||
@@ -43,26 +46,46 @@ export class OidcTokenRefresher {
|
||||
/**
|
||||
* The OIDC issuer as returned by the /auth_issuer API
|
||||
*/
|
||||
issuer: string,
|
||||
private issuer: string,
|
||||
/**
|
||||
* id of this client as registered with the OP
|
||||
*/
|
||||
clientId: string,
|
||||
private clientId: string,
|
||||
/**
|
||||
* redirectUri as registered with OP
|
||||
*/
|
||||
redirectUri: string,
|
||||
private redirectUri: string,
|
||||
/**
|
||||
* Device ID of current session
|
||||
*/
|
||||
deviceId: string,
|
||||
protected deviceId: string,
|
||||
/**
|
||||
* idTokenClaims as returned from authorization grant
|
||||
* used to validate tokens
|
||||
*/
|
||||
private readonly idTokenClaims: IdTokenClaims,
|
||||
) {
|
||||
this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri);
|
||||
this.oidcClientReady = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the client is initialised.
|
||||
* @returns Promise that resolves when initialisation is complete
|
||||
* @throws if initialisation fails
|
||||
*/
|
||||
private async ensureInit(): Promise<void> {
|
||||
if (!this.oidcClient) {
|
||||
if (this.initPromise) {
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
this.initPromise = this.initialiseOidcClient(this.issuer, this.clientId, this.deviceId, this.redirectUri);
|
||||
try {
|
||||
await this.initPromise;
|
||||
} finally {
|
||||
this.initPromise = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initialiseOidcClient(
|
||||
@@ -98,6 +121,8 @@ export class OidcTokenRefresher {
|
||||
* @throws when token refresh fails
|
||||
*/
|
||||
public async doRefreshAccessToken(refreshToken: string): Promise<AccessTokens> {
|
||||
await this.ensureInit();
|
||||
|
||||
if (!this.inflightRefreshRequest) {
|
||||
this.inflightRefreshRequest = this.getNewTokens(refreshToken);
|
||||
}
|
||||
@@ -123,7 +148,7 @@ export class OidcTokenRefresher {
|
||||
* @param tokens.accessToken - new access token
|
||||
* @param tokens.refreshToken - OPTIONAL new refresh token
|
||||
*/
|
||||
public async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise<void> {
|
||||
protected async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise<void> {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user