Skip to content

Commit

Permalink
Remove autorenew, request new token if request fails with permission …
Browse files Browse the repository at this point in the history
…error (#14)

* Fix token renew and renew token on vault request permission error

* do not retry token renew requests to prevent loops

* refactor request parameters

* remove autoRenew feature

* Update src/Vault.ts

Co-authored-by: Marco Falkenberg <[email protected]>

Co-authored-by: Marco Falkenberg <[email protected]>
  • Loading branch information
Lucaber and mfal authored Oct 12, 2020
1 parent c7a681d commit 9dfe1f8
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 65 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
45 changes: 34 additions & 11 deletions src/Vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,20 +65,20 @@ export class Vault {
return this.config.vaultToken;
}

public async read(path: string | string[], parameters?: HTTPGETParameters, acceptedReturnCodes?: number[]): Promise<any> {
return this.request("GET", path, {}, parameters, acceptedReturnCodes);
public async read(path: string | string[], parameters?: HTTPGETParameters, options?: VaultRequestOptions): Promise<any> {
return this.request("GET", path, {}, parameters, options);
}

public async write(path: string | string[], body: any, acceptedReturnCodes?: number[]): Promise<any> {
return this.request("POST", path, body, undefined, acceptedReturnCodes);
public async write(path: string | string[], body: any, options?: VaultRequestOptions): Promise<any> {
return this.request("POST", path, body, undefined, options);
}

public async delete(path: string | string[], body: any, acceptedReturnCodes?: number[]): Promise<any> {
return this.request("DELETE", path, body, undefined, acceptedReturnCodes);
public async delete(path: string | string[], body: any, options?: VaultRequestOptions): Promise<any> {
return this.request("DELETE", path, body, undefined, options);
}

public async list(path: string | string[], acceptedReturnCodes?: number[]): Promise<any> {
return this.request("LIST", path, {}, undefined, acceptedReturnCodes);
public async list(path: string | string[], options?: VaultRequestOptions): Promise<any> {
return this.request("LIST", path, {}, undefined, options);
}

public Health(): VaultHealthClient {
Expand Down Expand Up @@ -113,8 +118,14 @@ export class Vault {
path: string | string[],
body: any,
parameters?: HTTPGETParameters,
acceptedReturnCodes: number[] = [200, 204],
options?: VaultRequestOptions,
): Promise<any> {
options = {
retryWithTokenRenew: true,
acceptedReturnCodes: [200, 204],
...options,
};

if (typeof path === "string") {
path = [path];
}
Expand All @@ -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,
};
Expand Down
18 changes: 9 additions & 9 deletions src/VaultClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTTPGETParameters, Vault } from "./Vault";
import { HTTPGETParameters, Vault, VaultRequestOptions } from "./Vault";

export abstract class AbstractVaultClient {
private readonly mountPoint: string[];
Expand All @@ -9,19 +9,19 @@ export abstract class AbstractVaultClient {
this.vault = vault;
}

protected async rawRead(path: string[], parameters?: HTTPGETParameters, acceptedReturnCodes?: number[]): Promise<any> {
return this.vault.read([...this.mountPoint, ...path], parameters, acceptedReturnCodes);
protected async rawRead(path: string[], parameters?: HTTPGETParameters, options?: VaultRequestOptions): Promise<any> {
return this.vault.read([...this.mountPoint, ...path], parameters, options);
}

protected async rawWrite(path: string[], body?: any, acceptedReturnCodes?: number[]): Promise<any> {
return this.vault.write([...this.mountPoint, ...path], body, acceptedReturnCodes);
protected async rawWrite(path: string[], body?: any, options?: VaultRequestOptions): Promise<any> {
return this.vault.write([...this.mountPoint, ...path], body, options);
}

protected async rawDelete(path: string[], body?: any, acceptedReturnCodes?: number[]): Promise<any> {
return this.vault.delete([...this.mountPoint, ...path], body, acceptedReturnCodes);
protected async rawDelete(path: string[], body?: any, options?: VaultRequestOptions): Promise<any> {
return this.vault.delete([...this.mountPoint, ...path], body, options);
}

protected async rawList(path: string[], acceptedReturnCodes?: number[]): Promise<any> {
return this.vault.list([...this.mountPoint, ...path], acceptedReturnCodes);
protected async rawList(path: string[], options?: VaultRequestOptions): Promise<any> {
return this.vault.list([...this.mountPoint, ...path], options);
}
}
4 changes: 3 additions & 1 deletion src/auth/kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
50 changes: 9 additions & 41 deletions src/auth/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AutoRenewErrorHandler>();
private autoRenewEnabled = false;

public constructor(vault: Vault, mountPoint: string = "token", authProvider?: IVaultAuthProvider) {
super(vault, ["auth", mountPoint]);
Expand Down Expand Up @@ -52,7 +44,9 @@ export class VaultTokenClient extends AbstractVaultClient {
public async renewSelf(options?: IVaultTokenRenewSelfOptions, authProviderFallback: boolean = false): Promise<IVaultTokenAuthResponse> {
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;
});
Expand All @@ -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<IVaultTokenAuthResponse> {
return this.autoRenew(onError);
}

private async autoRenew(onError?: AutoRenewErrorHandler): Promise<IVaultTokenAuthResponse> {
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<IVaultTokenAuthResponse> {
if (!this.authProvider) {
throw new Error("No Authprovider configured");
}

return result;
this.state = await this.authProvider.auth();
return this.state;
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/sys/VaultHealthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export class VaultHealthClient extends AbstractVaultClient {
* Throws an VaultRequestError if vault is unhealthy
*/
public async health(): Promise<IVaultHealthResponse> {
return this.rawRead(["/health"], undefined, [200, 429]);
return this.rawRead(["/health"], undefined, {
acceptedReturnCodes: [200, 429],
});
}
}

0 comments on commit 9dfe1f8

Please sign in to comment.