Skip to content

Commit 8c45f6b

Browse files
authored
feat: add skip OIDC option (#1458)
1 parent a5c87b6 commit 8c45f6b

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ See [action.yml](./action.yml) for more detail.
150150
| retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No |
151151
| special-characters-workaround | Uncommonly, some environments cannot tolerate special characters in a secret key. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. | No |
152152
| use-existing-credentials | When set, the action will check if existing credentials are valid and exit if they are. Defaults to false. | No |
153+
| force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No |
153154
</details>
154155

155156
#### Adjust the retry mechanism

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ inputs:
7979
required: false
8080
use-existing-credentials:
8181
description: When enabled, this option will check if there are already valid credentials in the environment. If there are, new credentials will not be fetched. If there are not, the action will run as normal.
82+
force-skip-oidc:
83+
required: false
84+
description: When enabled, this option will skip using GitHub OIDC provider even if the id-token permission is set. This is sometimes useful when using IAM instance credentials.
85+
8286
outputs:
8387
aws-account-id:
8488
description: The AWS account ID for the provided credentials

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ export async function run() {
5151
const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false });
5252
const useExistingCredentials = core.getInput('use-existing-credentials', { required: false });
5353
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
54+
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
55+
56+
if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) {
57+
throw new Error(
58+
"If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set",
59+
);
60+
}
5461

5562
if (specialCharacterWorkaround) {
5663
// 😳
@@ -62,6 +69,7 @@ export async function run() {
6269

6370
// Logic to decide whether to attempt to use OIDC or not
6471
const useGitHubOIDCProvider = () => {
72+
if (forceSkipOidc) return false;
6573
// The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted.
6674
// This is necessary to authenticate with OIDC, but not strictly set just for OIDC. If it is not set and all other
6775
// checks pass, it is likely but not guaranteed that the user needs but lacks this permission in their workflow.

test/index.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,135 @@ describe('Configure AWS Credentials', {}, () => {
334334
});
335335
});
336336

337+
describe('Force Skip OIDC', {}, () => {
338+
beforeEach(() => {
339+
vi.clearAllMocks();
340+
mockedSTSClient.reset();
341+
});
342+
343+
it('skips OIDC when force-skip-oidc is true with IAM credentials', async () => {
344+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
345+
...mocks.IAM_ASSUMEROLE_INPUTS,
346+
'force-skip-oidc': 'true'
347+
}));
348+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
349+
mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS);
350+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
351+
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
352+
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials')
353+
.mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' })
354+
.mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' });
355+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
356+
357+
await run();
358+
expect(core.getIDToken).not.toHaveBeenCalled();
359+
expect(core.setFailed).not.toHaveBeenCalled();
360+
});
361+
362+
it('skips OIDC when force-skip-oidc is true with web identity token file', async () => {
363+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
364+
...mocks.WEBIDENTITY_TOKEN_FILE_INPUTS,
365+
'force-skip-oidc': 'true'
366+
}));
367+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
368+
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
369+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
370+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
371+
vi.mock('node:fs');
372+
vol.reset();
373+
fs.mkdirSync('/home/github', { recursive: true });
374+
fs.writeFileSync('/home/github/file.txt', 'test-token');
375+
376+
await run();
377+
expect(core.getIDToken).not.toHaveBeenCalled();
378+
expect(core.info).toHaveBeenCalledWith('Assuming role with web identity token file');
379+
expect(core.setFailed).not.toHaveBeenCalled();
380+
});
381+
382+
it('fails when force-skip-oidc is true but no alternative credentials provided', async () => {
383+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
384+
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
385+
'aws-region': 'fake-region-1',
386+
'force-skip-oidc': 'true'
387+
}));
388+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
389+
390+
await run();
391+
expect(core.setFailed).toHaveBeenCalledWith(
392+
"If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set"
393+
);
394+
});
395+
396+
it('allows force-skip-oidc without role-to-assume', async () => {
397+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
398+
...mocks.IAM_USER_INPUTS,
399+
'force-skip-oidc': 'true'
400+
}));
401+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
402+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
403+
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
404+
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({
405+
accessKeyId: 'MYAWSACCESSKEYID',
406+
});
407+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
408+
409+
await run();
410+
expect(core.getIDToken).not.toHaveBeenCalled();
411+
expect(core.info).toHaveBeenCalledWith('Proceeding with IAM user credentials');
412+
expect(core.setFailed).not.toHaveBeenCalled();
413+
});
414+
415+
it('uses OIDC when force-skip-oidc is false (default behavior)', async () => {
416+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
417+
...mocks.GH_OIDC_INPUTS,
418+
'force-skip-oidc': 'false'
419+
}));
420+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
421+
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
422+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
423+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
424+
425+
await run();
426+
expect(core.getIDToken).toHaveBeenCalledWith('');
427+
expect(core.info).toHaveBeenCalledWith('Assuming role with OIDC');
428+
expect(core.setFailed).not.toHaveBeenCalled();
429+
});
430+
431+
it('uses OIDC when force-skip-oidc is not set (default behavior)', async () => {
432+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.GH_OIDC_INPUTS));
433+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
434+
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
435+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
436+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
437+
438+
await run();
439+
expect(core.getIDToken).toHaveBeenCalledWith('');
440+
expect(core.info).toHaveBeenCalledWith('Assuming role with OIDC');
441+
expect(core.setFailed).not.toHaveBeenCalled();
442+
});
443+
444+
it('works with role chaining when force-skip-oidc is true', async () => {
445+
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({
446+
...mocks.EXISTING_ROLE_INPUTS,
447+
'force-skip-oidc': 'true',
448+
'aws-access-key-id': 'MYAWSACCESSKEYID',
449+
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY'
450+
}));
451+
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
452+
mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS);
453+
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
454+
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
455+
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials')
456+
.mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' })
457+
.mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' });
458+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
459+
460+
await run();
461+
expect(core.getIDToken).not.toHaveBeenCalled();
462+
expect(core.setFailed).not.toHaveBeenCalled();
463+
});
464+
});
465+
337466
describe('HTTP Proxy Configuration', {}, () => {
338467
beforeEach(() => {
339468
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.GH_OIDC_INPUTS));

0 commit comments

Comments
 (0)