Skip to content

Commit 74deacc

Browse files
committed
fix: back off failed OIDC refresh attempts
1 parent b5ecfe3 commit 74deacc

2 files changed

Lines changed: 32 additions & 7 deletions

File tree

src/credentials.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ky from "ky";
55

66
const DEFAULT_STS_ENDPOINT = "sts.aliyuncs.com";
77
const DEFAULT_REFRESH_BEFORE_EXPIRATION_SECONDS = 300;
8+
const DEFAULT_REFRESH_FAILURE_BACKOFF_SECONDS = 60;
89
const DEFAULT_REQUEST_TIMEOUT = 30000;
910
const DEFAULT_ROLE_SESSION_NAME = "alicloud-tablestore";
1011

@@ -18,6 +19,7 @@ export interface OIDCCredentialProviderConfig {
1819
durationSeconds?: number;
1920
stsEndpoint?: string;
2021
refreshBeforeExpirationSeconds?: number;
22+
refreshFailureBackoffSeconds?: number;
2123
requestTimeout?: number;
2224
}
2325

@@ -39,6 +41,7 @@ interface OIDCCredentials extends Credentials {
3941

4042
export class OIDCCredentialProvider {
4143
private credentials: OIDCCredentials | null = null;
44+
private refreshBlockedUntil = 0;
4245
private refreshPromise: Promise<Credentials> | null = null;
4346

4447
public constructor(private readonly config: OIDCCredentialProviderConfig) {
@@ -49,6 +52,10 @@ export class OIDCCredentialProvider {
4952
return this.credentials;
5053
}
5154

55+
if (this.credentials && !this.isExpired(this.credentials) && this.refreshBlockedUntil > Date.now()) {
56+
return this.credentials;
57+
}
58+
5259
if (!this.refreshPromise) {
5360
this.refreshPromise = this.refresh();
5461
}
@@ -58,6 +65,7 @@ export class OIDCCredentialProvider {
5865
}
5966
catch (error) {
6067
if (this.credentials && !this.isExpired(this.credentials)) {
68+
this.refreshBlockedUntil = Date.now() + this.getRefreshFailureBackoffMilliseconds();
6169
return this.credentials;
6270
}
6371

@@ -121,6 +129,7 @@ export class OIDCCredentialProvider {
121129
expiration,
122130
stsToken: credentials.SecurityToken,
123131
};
132+
this.refreshBlockedUntil = 0;
124133

125134
return this.credentials;
126135
}
@@ -148,6 +157,13 @@ export class OIDCCredentialProvider {
148157
private isExpired(credentials: OIDCCredentials): boolean {
149158
return credentials.expiration.getTime() <= Date.now();
150159
}
160+
161+
private getRefreshFailureBackoffMilliseconds(): number {
162+
return Math.max(
163+
0,
164+
this.config.refreshFailureBackoffSeconds ?? DEFAULT_REFRESH_FAILURE_BACKOFF_SECONDS,
165+
) * 1000;
166+
}
151167
}
152168

153169
export function createOIDCCredentialProvider(config: OIDCCredentialProviderConfig): CredentialProvider {
@@ -199,11 +215,15 @@ function parseSTSResponse(text: string): AssumeRoleWithOIDCResponse {
199215
}
200216

201217
function getSTSEndpointURL(endpoint = DEFAULT_STS_ENDPOINT): string {
202-
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
203-
return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
218+
const url = new URL(endpoint.startsWith("http://") || endpoint.startsWith("https://")
219+
? endpoint
220+
: `https://${endpoint}`);
221+
222+
if (!url.pathname.endsWith("/")) {
223+
url.pathname = `${url.pathname}/`;
204224
}
205225

206-
return `https://${endpoint}/`;
226+
return url.toString();
207227
}
208228

209229
function formatSTSError(status: number, statusText: string, data: AssumeRoleWithOIDCResponse): string {

test/credentials.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { createOIDCCredentialProvider } from "../src/credentials";
44
describe("OIDC credential provider", () => {
55
test("exchanges an OIDC token for STS credentials", async () => {
66
const requests: URLSearchParams[] = [];
7-
const restoreFetch = mockSTSFetch((params) => {
7+
const urls: string[] = [];
8+
const restoreFetch = mockSTSFetch((params, request) => {
89
requests.push(params);
10+
urls.push(request.url);
911
return createSTSResponse({
1012
accessKeyID: "access-key-id",
1113
accessKeySecret: "access-key-secret",
@@ -24,7 +26,7 @@ describe("OIDC credential provider", () => {
2426
}),
2527
roleArn: "acs:ram::1234567890123456:role/example",
2628
roleSessionName: "tablestore-test",
27-
stsEndpoint: "https://sts.example.com",
29+
stsEndpoint: "https://sts.example.com?source=test",
2830
});
2931

3032
const credentials = await credentialProvider();
@@ -36,6 +38,7 @@ describe("OIDC credential provider", () => {
3638
stsToken: "security-token",
3739
});
3840
expect(requests).toHaveLength(1);
41+
expect(urls).toEqual(["https://sts.example.com/?source=test"]);
3942
expect(requests[0]!.get("Action")).toBe("AssumeRoleWithOIDC");
4043
expect(requests[0]!.get("Version")).toBe("2015-04-01");
4144
expect(requests[0]!.get("OIDCToken")).toBe("oidc-token");
@@ -150,11 +153,13 @@ describe("OIDC credential provider", () => {
150153

151154
const first = await credentialProvider();
152155
const fallback = await credentialProvider();
156+
const backedOff = await credentialProvider();
153157

154158
expect(hits).toBe(2);
155159
expect(first.accessKeyID).toBe("access-key-id");
156160
expect(fallback.accessKeyID).toBe("access-key-id");
157161
expect(fallback.stsToken).toBe("security-token");
162+
expect(backedOff.accessKeyID).toBe("access-key-id");
158163
}
159164
finally {
160165
restoreFetch();
@@ -169,12 +174,12 @@ interface STSResponseOptions {
169174
stsToken: string;
170175
}
171176

172-
function mockSTSFetch(handler: (params: URLSearchParams) => unknown | Promise<unknown>): () => void {
177+
function mockSTSFetch(handler: (params: URLSearchParams, request: Request) => unknown | Promise<unknown>): () => void {
173178
const originalFetch = globalThis.fetch;
174179
globalThis.fetch = (async (input, init) => {
175180
const request = input instanceof Request ? input : new Request(String(input), init);
176181
const params = new URLSearchParams(await request.text());
177-
return Response.json(await handler(params));
182+
return Response.json(await handler(params, request));
178183
}) as typeof fetch;
179184

180185
return () => {

0 commit comments

Comments
 (0)