Skip to content

Commit

Permalink
feat: #251 retry silent renew for fetch timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
pamapa committed Jan 20, 2022
1 parent 70c5e94 commit 3b034f9
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 16 deletions.
5 changes: 4 additions & 1 deletion docs/oidc-client-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ export interface OidcClientSettings {
post_logout_redirect_uri?: string;
prompt?: string;
redirect_uri: string;
requestTimeoutInSeconds?: number;
resource?: string;
response_mode?: "query" | "fragment";
response_type?: string;
Expand All @@ -353,7 +354,7 @@ export interface OidcClientSettings {

// @public
export class OidcClientSettingsStore {
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, stateStore, extraQueryParams, extraTokenParams, }: OidcClientSettings);
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, requestTimeoutInSeconds, userInfoJwtIssuer, mergeClaims, stateStore, extraQueryParams, extraTokenParams, }: OidcClientSettings);
// (undocumented)
readonly acr_values: string | undefined;
// (undocumented)
Expand Down Expand Up @@ -393,6 +394,8 @@ export class OidcClientSettingsStore {
// (undocumented)
readonly redirect_uri: string;
// (undocumented)
readonly requestTimeoutInSeconds: number;
// (undocumented)
readonly resource: string | undefined;
// (undocumented)
readonly response_mode: "query" | "fragment";
Expand Down
16 changes: 12 additions & 4 deletions src/JsonService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { ErrorResponse } from "./errors";
import { OidcClientSettings, OidcClientSettingsStore } from "./OidcClientSettings";
import { JsonService } from "./JsonService";

import { mocked } from "jest-mock";

describe("JsonService", () => {
let settings: OidcClientSettings;
let subject: JsonService;

beforeEach(() =>{
subject = new JsonService();
settings = {
authority: "authority",
client_id: "client",
redirect_uri: "redirect",
};
subject = new JsonService(new OidcClientSettingsStore(settings));
});

describe("getJson", () => {
Expand Down Expand Up @@ -118,7 +126,7 @@ describe("JsonService", () => {

it("should accept custom content type in response", async () => {
// arrange
subject = new JsonService(["foo/bar"]);
subject = new JsonService(new OidcClientSettingsStore(settings), ["foo/bar"]);
const json = { foo: 1, bar: "test" };
mocked(fetch).mockResolvedValue({
status: 200,
Expand All @@ -139,7 +147,7 @@ describe("JsonService", () => {
it("should work with custom jwtHandler", async () => {
// arrange
const jwtHandler = jest.fn();
subject = new JsonService([], jwtHandler);
subject = new JsonService(new OidcClientSettingsStore(settings), [], jwtHandler);
const text = "text";
mocked(fetch).mockResolvedValue({
status: 200,
Expand Down Expand Up @@ -381,7 +389,7 @@ describe("JsonService", () => {

it("should accept custom content type in response", async () => {
// arrange
subject = new JsonService(["foo/bar"]);
subject = new JsonService(new OidcClientSettingsStore(settings), ["foo/bar"]);
const json = { foo: 1, bar: "test" };
mocked(fetch).mockResolvedValue({
status: 200,
Expand Down
19 changes: 16 additions & 3 deletions src/JsonService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { ErrorResponse } from "./errors";
import { ErrorResponse, ErrorTimeout } from "./errors";
import type { OidcClientSettingsStore } from "./OidcClientSettings";
import { Logger } from "./utils";

/**
Expand All @@ -18,6 +19,7 @@ export class JsonService {
private _contentTypes: string[] = [];

public constructor(
private readonly _settings: OidcClientSettingsStore,
additionalContentTypes: string[] = [],
private _jwtHandler: JwtHandler | null = null,
) {
Expand All @@ -27,6 +29,17 @@ export class JsonService {
}
}

protected async fetchWithTimeout(input: RequestInfo, init?: RequestInit) {
return await Promise.race([
fetch(input, init),
new Promise<Response>((_, reject) => {
setTimeout(() => {
reject(new ErrorTimeout("Network timed out"));
}, this._settings.requestTimeoutInSeconds * 1000);
}),
]);
}

public async getJson(url: string, token?: string): Promise<Record<string, unknown>> {
const logger = this._logger.create("getJson");
const headers: HeadersInit = {
Expand All @@ -40,7 +53,7 @@ export class JsonService {
let response: Response;
try {
logger.debug("url:", url);
response = await fetch(url, { method: "GET", headers });
response = await this.fetchWithTimeout(url, { method: "GET", headers });
}
catch (err) {
logger.error("Network Error");
Expand Down Expand Up @@ -87,7 +100,7 @@ export class JsonService {
let response: Response;
try {
logger.debug("url:", url);
response = await fetch(url, { method: "POST", headers, body });
response = await this.fetchWithTimeout(url, { method: "POST", headers, body });
}
catch (err) {
logger.error("Network error");
Expand Down
4 changes: 3 additions & 1 deletion src/MetadataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ const OidcMetadataUrlPath = ".well-known/openid-configuration";
*/
export class MetadataService {
private readonly _logger = new Logger("MetadataService");
private readonly _jsonService = new JsonService(["application/jwk-set+json"]);
private readonly _jsonService: JsonService;

// cache
private _metadataUrl: string | null = null;
private _signingKeys: SigningKey[] | null = null;
private _metadata: Partial<OidcMetadata> | null = null;

public constructor(private readonly _settings: OidcClientSettingsStore) {
this._jsonService = new JsonService(_settings, ["application/jwk-set+json"]);

if (this._settings.metadataUrl) {
this._metadataUrl = this._settings.metadataUrl;
} else if (this._settings.authority) {
Expand Down
7 changes: 6 additions & 1 deletion src/OidcClientSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const DefaultClientAuthentication = "client_secret_post";
const DefaultResponseMode = "query";
const DefaultStaleStateAgeInSeconds = 60 * 15;
const DefaultClockSkewInSeconds = 60 * 5;
const DefaultRequestTimeoutInSeconds = 8;

/**
* @public
Expand Down Expand Up @@ -79,8 +80,9 @@ export interface OidcClientSettings {
staleStateAgeInSeconds?: number;
/** The window of time (in seconds) to allow the current time to deviate when validating token's iat, nbf, and exp values (default: 300) */
clockSkewInSeconds?: number;
/** Number of seconds to wait for a network request to return before assuming it has timed out (default: 8) */
requestTimeoutInSeconds?: number;
userInfoJwtIssuer?: "ANY" | "OP" | string;

/**
* Indicates if objects returned from the user info endpoint as claims (e.g. `address`) are merged into the claims from the id token as a single object.
* Otherwise, they are added to an array as distinct objects for the claim type. (default: false)
Expand Down Expand Up @@ -139,6 +141,7 @@ export class OidcClientSettingsStore {
public readonly loadUserInfo: boolean;
public readonly staleStateAgeInSeconds: number;
public readonly clockSkewInSeconds: number;
public readonly requestTimeoutInSeconds: number;
public readonly userInfoJwtIssuer: "ANY" | "OP" | string;
public readonly mergeClaims: boolean;

Expand All @@ -162,6 +165,7 @@ export class OidcClientSettingsStore {
loadUserInfo = false,
staleStateAgeInSeconds = DefaultStaleStateAgeInSeconds,
clockSkewInSeconds = DefaultClockSkewInSeconds,
requestTimeoutInSeconds = DefaultRequestTimeoutInSeconds,
userInfoJwtIssuer = "OP",
mergeClaims = false,
// other behavior
Expand Down Expand Up @@ -197,6 +201,7 @@ export class OidcClientSettingsStore {
this.loadUserInfo = !!loadUserInfo;
this.staleStateAgeInSeconds = staleStateAgeInSeconds;
this.clockSkewInSeconds = clockSkewInSeconds;
this.requestTimeoutInSeconds = requestTimeoutInSeconds;
this.userInfoJwtIssuer = userInfoJwtIssuer;
this.mergeClaims = !!mergeClaims;

Expand Down
2 changes: 1 addition & 1 deletion src/ResponseValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const ProtocolClaims = [
*/
export class ResponseValidator {
protected readonly _logger = new Logger("ResponseValidator");
protected readonly _userInfoService = new UserInfoService(this._metadataService);
protected readonly _userInfoService = new UserInfoService(this._settings, this._metadataService);
protected readonly _tokenClient = new TokenClient(this._settings, this._metadataService);

public constructor(
Expand Down
6 changes: 4 additions & 2 deletions src/TokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ export interface RevokeArgs {
*/
export class TokenClient {
private readonly _logger = new Logger("TokenClient");
private readonly _jsonService = new JsonService();
private readonly _jsonService: JsonService;

public constructor(
private readonly _settings: OidcClientSettingsStore,
private readonly _metadataService: MetadataService,
) {}
) {
this._jsonService = new JsonService(_settings);
}

public async exchangeCode({
grant_type = "authorization_code",
Expand Down
2 changes: 1 addition & 1 deletion src/UserInfoService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("UserInfoService", () => {
});
metadataService = new MetadataService(settings);

subject = new UserInfoService(metadataService);
subject = new UserInfoService(settings, metadataService);

// access private members
jsonService = subject["_jsonService"];
Expand Down
8 changes: 6 additions & 2 deletions src/UserInfoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { Logger, JwtUtils } from "./utils";
import type { OidcClientSettingsStore } from "./OidcClientSettings";
import { JsonService } from "./JsonService";
import type { MetadataService } from "./MetadataService";
import type { JwtClaims } from "./Claims";
Expand All @@ -13,8 +14,11 @@ export class UserInfoService {
protected readonly _logger = new Logger("UserInfoService");
private readonly _jsonService: JsonService;

public constructor(private readonly _metadataService: MetadataService) {
this._jsonService = new JsonService(undefined, this._getClaimsFromJwt);
public constructor(
settings: OidcClientSettingsStore,
private readonly _metadataService: MetadataService,
) {
this._jsonService = new JsonService(settings, undefined, this._getClaimsFromJwt);
}

public async getClaims(token: string): Promise<JwtClaims> {
Expand Down

0 comments on commit 3b034f9

Please sign in to comment.