mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-08-25 04:35:06 +09:00
Compare commits
1 Commits
v6.2.3
...
kellertk/1848
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a97d6e55 |
@@ -10,6 +10,10 @@ if (!process.env.AWS_EXECUTION_ENV) {
|
||||
process.env.AWS_EXECUTION_ENV = 'GitHubActions';
|
||||
}
|
||||
|
||||
// Bound how long a single STS call may hang. 60s per attempt keeps the total
|
||||
// failure time predictable.
|
||||
const STS_TIMEOUT_MS = 60_000;
|
||||
|
||||
export interface CredentialsClientProps {
|
||||
region?: string;
|
||||
proxyServer?: string;
|
||||
@@ -43,6 +47,14 @@ export class CredentialsClient {
|
||||
this.requestHandler = new NodeHttpHandler({
|
||||
httpsAgent: handler,
|
||||
httpAgent: handler,
|
||||
connectionTimeout: STS_TIMEOUT_MS,
|
||||
requestTimeout: STS_TIMEOUT_MS,
|
||||
});
|
||||
} else {
|
||||
// No proxy
|
||||
this.requestHandler = new NodeHttpHandler({
|
||||
connectionTimeout: STS_TIMEOUT_MS,
|
||||
requestTimeout: STS_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
if (props.stsEndpoint) {
|
||||
|
||||
@@ -281,6 +281,22 @@ export function isDefined<T>(i: T | undefined | null): i is T {
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Reads the `exp` claim (Unix seconds) from a JWT and reports whether the token is already expired or will expire
|
||||
// within `skewSeconds`. This is to decide whether to re-mint the OIDC token before an AssumeRole attempt. On any parse
|
||||
// failure we return false so a malformed token can't start a re-mint loop.
|
||||
export function jwtExpiresWithin(token: string, skewSeconds: number): boolean {
|
||||
try {
|
||||
const payload = token.split('.')[1];
|
||||
if (!payload) return false;
|
||||
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
||||
if (typeof decoded.exp !== 'number') return false;
|
||||
const nowSeconds = Date.now() / 1000;
|
||||
return decoded.exp <= nowSeconds + skewSeconds;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function areCredentialsValid(credentialsClient: CredentialsClient) {
|
||||
const client = credentialsClient.stsClient;
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
exportCredentials,
|
||||
exportRegion,
|
||||
getBooleanInput,
|
||||
jwtExpiresWithin,
|
||||
retryAndBackoff,
|
||||
toCredentialIdentity,
|
||||
translateEnvVariables,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
import { writeProfileFiles } from './profileManager';
|
||||
|
||||
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
|
||||
const TOKEN_REFRESH_SKEW_SECONDS = 30;
|
||||
const ROLE_SESSION_NAME = 'GitHubActions';
|
||||
const REGION_REGEX = /^[a-z0-9-]+$/g;
|
||||
const ROLE_SESSION_NAME_REGEX = /^[\w+=,.@-]*$/;
|
||||
@@ -246,6 +248,10 @@ export async function run() {
|
||||
let roleCredentials: AssumeRoleCommandOutput;
|
||||
do {
|
||||
roleCredentials = await withRetry(async () => {
|
||||
if (useGitHubOIDCProvider() && jwtExpiresWithin(webIdentityToken, TOKEN_REFRESH_SKEW_SECONDS)) {
|
||||
core.info('OIDC token has expired or is about to; requesting a fresh one before AssumeRole.');
|
||||
webIdentityToken = await core.getIDToken(audience);
|
||||
}
|
||||
return assumeRole({
|
||||
credentialsClient,
|
||||
sourceAccountId,
|
||||
|
||||
@@ -51,6 +51,33 @@ describe('Configure AWS Credentials helpers', {}, () => {
|
||||
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Retry: attempt 1 of 3 failed'));
|
||||
helpers.reset();
|
||||
});
|
||||
describe('jwtExpiresWithin', {}, () => {
|
||||
// Builds a JWT-shaped string (header.payload.signature) with the given payload. Only the payload
|
||||
// segment is read by the helper; the signature is irrelevant since we never verify it.
|
||||
const makeJwt = (payload: object) => {
|
||||
const encode = (obj: object) => Buffer.from(JSON.stringify(obj)).toString('base64url');
|
||||
return `${encode({ alg: 'RS256' })}.${encode(payload)}.signature`;
|
||||
};
|
||||
it('returns true when the token is already expired', {}, () => {
|
||||
const exp = Math.floor(Date.now() / 1000) - 60;
|
||||
expect(helpers.jwtExpiresWithin(makeJwt({ exp }), 30)).toBe(true);
|
||||
});
|
||||
it('returns true when the token expires within the skew window', {}, () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 10;
|
||||
expect(helpers.jwtExpiresWithin(makeJwt({ exp }), 30)).toBe(true);
|
||||
});
|
||||
it('returns false when the token is comfortably valid', {}, () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 900;
|
||||
expect(helpers.jwtExpiresWithin(makeJwt({ exp }), 30)).toBe(false);
|
||||
});
|
||||
it('returns false for a token with no exp claim', {}, () => {
|
||||
expect(helpers.jwtExpiresWithin(makeJwt({ sub: 'foo' }), 30)).toBe(false);
|
||||
});
|
||||
it('returns false for a malformed token', {}, () => {
|
||||
expect(helpers.jwtExpiresWithin('not-a-jwt', 30)).toBe(false);
|
||||
expect(helpers.jwtExpiresWithin('', 30)).toBe(false);
|
||||
});
|
||||
});
|
||||
it('can output creds when told to', {}, () => {
|
||||
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
|
||||
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
|
||||
|
||||
@@ -89,6 +89,28 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
expect(core.exportVariable).toHaveBeenCalledTimes(5);
|
||||
expect(core.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
it('re-mints the OIDC token when it has expired during retries', {}, async () => {
|
||||
// Helper to build a JWT with a given exp (Unix seconds). Only the payload is read by the action.
|
||||
const makeJwt = (exp: number) => {
|
||||
const encode = (obj: object) => Buffer.from(JSON.stringify(obj)).toString('base64url');
|
||||
return `${encode({ alg: 'RS256' })}.${encode({ exp })}.sig`;
|
||||
};
|
||||
const expiredToken = makeJwt(Math.floor(Date.now() / 1000) - 60);
|
||||
const freshToken = makeJwt(Math.floor(Date.now() / 1000) + 900);
|
||||
// First call returns an already-expired token; the refresh inside the retry loop returns a fresh one.
|
||||
vi.mocked(core.getIDToken).mockResolvedValueOnce(expiredToken).mockResolvedValueOnce(freshToken);
|
||||
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS);
|
||||
await run();
|
||||
// Token fetched once up front, then re-minted because the first was expired.
|
||||
expect(core.getIDToken).toHaveBeenCalledTimes(2);
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
'OIDC token has expired or is about to; requesting a fresh one before AssumeRole.',
|
||||
);
|
||||
// The fresh token is the one actually sent to STS.
|
||||
const call = mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)[0];
|
||||
expect(call.args[0].input.WebIdentityToken).toBe(freshToken);
|
||||
expect(core.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('IAM User Authentication', {}, () => {
|
||||
|
||||
Reference in New Issue
Block a user