Skip to content

Commit

Permalink
feat: add support for github actions OIDC exchange (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshallOfSound authored Sep 17, 2024
1 parent fe75008 commit 63fc19c
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 5 deletions.
12 changes: 12 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,21 @@ export type CircleOIDCPlatform = OIDCPlatform<
}
>;

export type GitHubActionsOIDCPlatform = OIDCPlatform<
'github',
{
organizationId: string;
},
{
repositoryIds: string[];
environments: string[];
}
>;

export type InvalidOIDCPlatform = OIDCPlatform<'invalid', object, object>;

export type OIDCSecretExchangeConfig = (
| OIDCSecretExchangeConfigItem<CircleOIDCPlatform>
| OIDCSecretExchangeConfigItem<GitHubActionsOIDCPlatform>
| OIDCSecretExchangeConfigItem<InvalidOIDCPlatform>
)[];
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ export const configureAndListen = async (
);
break;
}
case 'github': {
filteredSecretProviders = await getProvidersForConfig(
req.log,
configuration,
perPlatformHandlers.github,
token,
);
break;
}
case 'invalid': {
filteredSecretProviders = await getProvidersForConfig(
req.log,
Expand Down
59 changes: 54 additions & 5 deletions src/oidc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { FastifyBaseLogger } from 'fastify';
import {
CircleOIDCPlatform,
GitHubActionsOIDCPlatform,
InvalidOIDCPlatform,
OIDCSecretExchangeConfig,
OIDCSecretExchangeConfigItem,
OIDCSecretExchangeConfiguration,
} from '../config';
import { BaseClaims, CircleCIOIDCClaims } from '../type';
import { BaseClaims, CircleCIOIDCClaims, GitHubActionsOIDCClaims } from '../type';
import { getValidatedToken } from './validate-token';

export type SubjectedSecretProvider = {
Expand All @@ -19,19 +20,19 @@ export type PlatformHandler<
TClaims extends BaseClaims,
> = {
discoveryUrlForToken: (config: TConfig, token: string) => string | null;
validateToken: (token: string, discoveryUrl: string) => Promise<TClaims | null>;
validateToken: (config: TConfig, token: string, discoveryUrl: string) => Promise<TClaims | null>;
filterSecretProviders: (config: TConfig, claims: TClaims) => Promise<SubjectedSecretProvider>;
};

const circleci: PlatformHandler<
OIDCSecretExchangeConfigItem<CircleOIDCPlatform>,
CircleCIOIDCClaims
> = {
discoveryUrlForToken: (config, token) => {
discoveryUrlForToken: (config) => {
// TODO: Pre-validate the token to claim this audience
return `https://oidc.circleci.com/org/${config.organizationId}`;
},
validateToken: async (token, discoveryUrl) => {
validateToken: async (_, token, discoveryUrl) => {
return await getValidatedToken(token, discoveryUrl);
},
filterSecretProviders: async (config, claims) => {
Expand Down Expand Up @@ -63,6 +64,53 @@ const circleci: PlatformHandler<
},
};

const github: PlatformHandler<
OIDCSecretExchangeConfigItem<GitHubActionsOIDCPlatform>,
GitHubActionsOIDCClaims
> = {
discoveryUrlForToken: () => {
return 'https://token.actions.githubusercontent.com';
},
validateToken: async (config, token, discoveryUrl) => {
const validated = await getValidatedToken<GitHubActionsOIDCClaims>(token, discoveryUrl);
if (validated) {
// If the token was validated but the owner doesn't match the org ID, the token is invalid
if (
!validated.repository_owner_id ||
validated.repository_owner_id !== config.organizationId
) {
return null;
}
}
return validated;
},
filterSecretProviders: async (config, claims) => {
const validatedEnvironment = claims.environment;
const repoId = claims.repository_id;
const ownerId = claims.repository_owner_id;
if (config.organizationId !== ownerId) return { subject: claims.sub, providers: [] };

const filteredSecretProviders = config.secrets
.filter((provider) => {
if (!provider.filters.repositoryIds.includes(repoId)) return false;
// If the wildcard environment is allowed then we don't need to check the environment filter
if (!provider.filters.environments.includes('*')) {
// If the environment is missing from the claim, don't validate it in case consumers accidentally put
// 'undefined' in the environments array
if (
!validatedEnvironment ||
!provider.filters.environments.includes(validatedEnvironment)
)
return false;
}
return true;
})
.map((p) => p.provider);

return { subject: claims.sub, providers: filteredSecretProviders };
},
};

const invalid: PlatformHandler<OIDCSecretExchangeConfigItem<InvalidOIDCPlatform>, BaseClaims> = {
discoveryUrlForToken: () => {
return null;
Expand All @@ -77,6 +125,7 @@ const invalid: PlatformHandler<OIDCSecretExchangeConfigItem<InvalidOIDCPlatform>

export const perPlatformHandlers = {
circleci,
github,
invalid,
};

Expand All @@ -93,7 +142,7 @@ export const getProvidersForConfig = async <
const discoveryUrl = handler.discoveryUrlForToken(config, token);
if (!discoveryUrl) return null;

const claims = await handler.validateToken(token, discoveryUrl);
const claims = await handler.validateToken(config, token, discoveryUrl);
if (!claims) return null;

logger.info(`Validated incoming OIDC token from: ${claims.sub}`);
Expand Down
19 changes: 19 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,22 @@ export type CircleCIOIDCClaims = {
*/
'oidc.circleci.com/context-ids': string[];
};

export type GitHubActionsOIDCClaims = {
/**
* The subject, this identifies the repo + org. It's value is "repo:foo/bar:environment:fee"
*/
sub: string;
/**
* Environment the job is running in, if no environment will be empty
*/
environment: string | null;
/**
* ID of the repo
*/
repository_id: string;
/**
* ID of the org
*/
repository_owner_id: string;
};

0 comments on commit 63fc19c

Please sign in to comment.