diff --git a/README.md b/README.md index 00f94e5..c1e120a 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ const k8sauth = client.KubernetesAuth({ role: "myrole", }); -const onAutoRenewError = (e) => console.error(e); -await client.Auth(k8sauth).enableAutoRenew(onAutoRenewError); +await client.Auth(k8sauth).login(); client .Health() diff --git a/src/Vault.ts b/src/Vault.ts index 40ea9ea..bc82145 100644 --- a/src/Vault.ts +++ b/src/Vault.ts @@ -40,6 +40,11 @@ export class VaultRequestError extends VaultError { } } +export interface VaultRequestOptions { + retryWithTokenRenew?: boolean; + acceptedReturnCodes?: number[]; +} + export class Vault { public readonly config: IVaultConfig; private tokenClient?: VaultTokenClient; @@ -60,20 +65,20 @@ export class Vault { return this.config.vaultToken; } - public async read(path: string | string[], parameters?: HTTPGETParameters, acceptedReturnCodes?: number[]): Promise { - return this.request("GET", path, {}, parameters, acceptedReturnCodes); + public async read(path: string | string[], parameters?: HTTPGETParameters, options?: VaultRequestOptions): Promise { + return this.request("GET", path, {}, parameters, options); } - public async write(path: string | string[], body: any, acceptedReturnCodes?: number[]): Promise { - return this.request("POST", path, body, undefined, acceptedReturnCodes); + public async write(path: string | string[], body: any, options?: VaultRequestOptions): Promise { + return this.request("POST", path, body, undefined, options); } - public async delete(path: string | string[], body: any, acceptedReturnCodes?: number[]): Promise { - return this.request("DELETE", path, body, undefined, acceptedReturnCodes); + public async delete(path: string | string[], body: any, options?: VaultRequestOptions): Promise { + return this.request("DELETE", path, body, undefined, options); } - public async list(path: string | string[], acceptedReturnCodes?: number[]): Promise { - return this.request("LIST", path, {}, undefined, acceptedReturnCodes); + public async list(path: string | string[], options?: VaultRequestOptions): Promise { + return this.request("LIST", path, {}, undefined, options); } public Health(): VaultHealthClient { @@ -113,8 +118,14 @@ export class Vault { path: string | string[], body: any, parameters?: HTTPGETParameters, - acceptedReturnCodes: number[] = [200, 204], + options?: VaultRequestOptions, ): Promise { + options = { + retryWithTokenRenew: true, + acceptedReturnCodes: [200, 204], + ...options, + }; + if (typeof path === "string") { path = [path]; } @@ -136,9 +147,21 @@ export class Vault { qs: parameters, }; - const res = await request(requestOptions); + let res = await request(requestOptions); + + if (this.tokenClient && options.retryWithTokenRenew && res.statusCode === 403) { + // token could be expired, try a new one + await this.tokenClient.login(); + res = await request({ + ...requestOptions, + headers: { + ...requestOptions.headers, + "X-Vault-Token": this.token, + }, + }); + } - if (!acceptedReturnCodes.some((c) => c === res.statusCode)) { + if (!options.acceptedReturnCodes?.includes(res.statusCode)) { let errorResponse: IVaultErrorResponse = { statusCode: res.statusCode, }; diff --git a/src/VaultClient.ts b/src/VaultClient.ts index 620abd8..ffcdb5f 100644 --- a/src/VaultClient.ts +++ b/src/VaultClient.ts @@ -1,4 +1,4 @@ -import { HTTPGETParameters, Vault } from "./Vault"; +import { HTTPGETParameters, Vault, VaultRequestOptions } from "./Vault"; export abstract class AbstractVaultClient { private readonly mountPoint: string[]; @@ -9,19 +9,19 @@ export abstract class AbstractVaultClient { this.vault = vault; } - protected async rawRead(path: string[], parameters?: HTTPGETParameters, acceptedReturnCodes?: number[]): Promise { - return this.vault.read([...this.mountPoint, ...path], parameters, acceptedReturnCodes); + protected async rawRead(path: string[], parameters?: HTTPGETParameters, options?: VaultRequestOptions): Promise { + return this.vault.read([...this.mountPoint, ...path], parameters, options); } - protected async rawWrite(path: string[], body?: any, acceptedReturnCodes?: number[]): Promise { - return this.vault.write([...this.mountPoint, ...path], body, acceptedReturnCodes); + protected async rawWrite(path: string[], body?: any, options?: VaultRequestOptions): Promise { + return this.vault.write([...this.mountPoint, ...path], body, options); } - protected async rawDelete(path: string[], body?: any, acceptedReturnCodes?: number[]): Promise { - return this.vault.delete([...this.mountPoint, ...path], body, acceptedReturnCodes); + protected async rawDelete(path: string[], body?: any, options?: VaultRequestOptions): Promise { + return this.vault.delete([...this.mountPoint, ...path], body, options); } - protected async rawList(path: string[], acceptedReturnCodes?: number[]): Promise { - return this.vault.list([...this.mountPoint, ...path], acceptedReturnCodes); + protected async rawList(path: string[], options?: VaultRequestOptions): Promise { + return this.vault.list([...this.mountPoint, ...path], options); } } diff --git a/src/auth/kubernetes.ts b/src/auth/kubernetes.ts index 97a4f82..e961667 100644 --- a/src/auth/kubernetes.ts +++ b/src/auth/kubernetes.ts @@ -28,7 +28,9 @@ export class VaultKubernetesAuthClient extends AbstractVaultClient implements IV if (!this.config.jwt) { this.initConfig(this.config); } - return this.rawWrite(["/login"], this.config).then((res) => { + return this.rawWrite(["/login"], this.config, { + retryWithTokenRenew: false, + }).then((res) => { tiChecker.IVaultTokenAuthResponse.check(res); return res; }); diff --git a/src/auth/token.ts b/src/auth/token.ts index 1eef94a..f1485ed 100644 --- a/src/auth/token.ts +++ b/src/auth/token.ts @@ -6,17 +6,9 @@ import { createCheckers } from "ts-interface-checker"; const tiChecker = createCheckers(tokenTi); -// Time in ms to renew token before expiration -const RENEW_BEFORE_MS = 10000; - -export type AutoRenewErrorHandler = (error: any) => void; - export class VaultTokenClient extends AbstractVaultClient { private state?: IVaultTokenAuthResponse; - private expires?: Date; private readonly authProvider?: IVaultAuthProvider; - private readonly autoRenewErrorHandlers = new Set(); - private autoRenewEnabled = false; public constructor(vault: Vault, mountPoint: string = "token", authProvider?: IVaultAuthProvider) { super(vault, ["auth", mountPoint]); @@ -52,7 +44,9 @@ export class VaultTokenClient extends AbstractVaultClient { public async renewSelf(options?: IVaultTokenRenewSelfOptions, authProviderFallback: boolean = false): Promise { let newState: IVaultTokenAuthResponse; try { - newState = await this.rawWrite(["/renew-self"], options).then((res) => { + newState = await this.rawWrite(["/renew-self"], options, { + retryWithTokenRenew: false, + }).then((res) => { tiChecker.IVaultTokenAuthResponse.check(res); return res; }); @@ -62,45 +56,19 @@ export class VaultTokenClient extends AbstractVaultClient { } newState = await this.authProvider.auth(); } - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + newState.auth.lease_duration); this.state = newState; - this.expires = expires; return this.state; } /** - * Enables a periodic job that renews the token before expiration. - * To receive renew errors, subscribe to the "error" event on the vault instance. + * Updates the token using the configured authProvider */ - public async enableAutoRenew(onError?: AutoRenewErrorHandler): Promise { - return this.autoRenew(onError); - } - - private async autoRenew(onError?: AutoRenewErrorHandler): Promise { - if (onError) { - this.autoRenewErrorHandlers.add(onError); - } - - const result = await this.renewSelf(undefined, true); - - if (!this.autoRenewEnabled) { - setTimeout(() => { - this.autoRenew().catch((error) => { - this.autoRenewErrorHandlers.forEach((handler) => { - try { - handler(error); - } catch (handlerCallError) { - // ignore errors from error handler - } - }); - }); - }, this.expires!.getTime() - new Date().getTime() - RENEW_BEFORE_MS); - - this.autoRenewEnabled = true; + public async login(): Promise { + if (!this.authProvider) { + throw new Error("No Authprovider configured"); } - - return result; + this.state = await this.authProvider.auth(); + return this.state; } } diff --git a/src/sys/VaultHealthClient.ts b/src/sys/VaultHealthClient.ts index e9fac9c..1018b2b 100644 --- a/src/sys/VaultHealthClient.ts +++ b/src/sys/VaultHealthClient.ts @@ -24,6 +24,8 @@ export class VaultHealthClient extends AbstractVaultClient { * Throws an VaultRequestError if vault is unhealthy */ public async health(): Promise { - return this.rawRead(["/health"], undefined, [200, 429]); + return this.rawRead(["/health"], undefined, { + acceptedReturnCodes: [200, 429], + }); } }