WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 37 additions & 41 deletions spec/unit/oidc/tokenRefresher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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("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(
"Cannot get new token before OIDC client is initialised.",
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");

Expand All @@ -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",
Expand Down
45 changes: 35 additions & 10 deletions src/oidc/tokenRefresher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,62 @@
*/
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>;

public constructor(
/**
* The OIDC issuer as returned by the /auth_issuer API
*/
issuer: string,
private issuer: string,

Check warning on line 49 in src/oidc/tokenRefresher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'issuer: string' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZsOA4S5aWqPbcg_RxOi&open=AZsOA4S5aWqPbcg_RxOi&pullRequest=5106
/**
* id of this client as registered with the OP
*/
clientId: string,
private clientId: string,

Check warning on line 53 in src/oidc/tokenRefresher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'clientId: string' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZsOA4S5aWqPbcg_RxOj&open=AZsOA4S5aWqPbcg_RxOj&pullRequest=5106
/**
* redirectUri as registered with OP
*/
redirectUri: string,
private redirectUri: string,

Check warning on line 57 in src/oidc/tokenRefresher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'redirectUri: string' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZsOA4S5aWqPbcg_RxOk&open=AZsOA4S5aWqPbcg_RxOk&pullRequest=5106
/**
* 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();

Check warning on line 68 in src/oidc/tokenRefresher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'oidcClientReady' is deprecated.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZsOA4S5aWqPbcg_RxOm&open=AZsOA4S5aWqPbcg_RxOm&pullRequest=5106

Check failure on line 68 in src/oidc/tokenRefresher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this asynchronous operation outside of the constructor.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZsOA4S5aWqPbcg_RxOl&open=AZsOA4S5aWqPbcg_RxOl&pullRequest=5106
}

/**
* 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(
Expand Down Expand Up @@ -98,6 +121,8 @@
* @throws when token refresh fails
*/
public async doRefreshAccessToken(refreshToken: string): Promise<AccessTokens> {
await this.ensureInit();

if (!this.inflightRefreshRequest) {
this.inflightRefreshRequest = this.getNewTokens(refreshToken);
}
Expand All @@ -123,7 +148,7 @@
* @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
}

Expand Down