Skip to content

Commit

Permalink
Improve OpenID Connect integration
Browse files Browse the repository at this point in the history
  • Loading branch information
yahavi committed Jan 19, 2024
1 parent 8fc3d00 commit 19cc8f8
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
"typescript.tsc.autoDetect": "off",
"workbench.editor.enablePreview": false,
"workbench.editor.enablePreviewFromQuickOpen": false,
"workbench.editor.showTabs": true,
"workbench.editor.showTabs": "multiple",
"files.autoSave": "afterDelay"
}
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ To utilize the OIDC protocol, follow these steps:
<div id="platformstep2"/>

2. **[Configure an identity mapping](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-identity-mappings)**: This phase generates a reference token for authenticating against the JFrog platform. It involves defining the necessary details to enable server authentication of the action issuer and granting the issuer an appropriate access token.
You have the flexibility to define any valid list of claims required for request authentication. You can check a list of the possible claims [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
Example Claims JSON:
```yml
You have the flexibility to define any valid list of claims required for request authentication. You can check a list of the possible claims [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
Example Claims JSON:
```json
{
"sub": "repo:my-user-name/project1:ref:refs/heads/main",
"aud": "https://github.com/my-user-name",
Expand All @@ -131,22 +131,23 @@ To utilize the OIDC protocol, follow these steps:
1. **Set required permissions**: In the course of the protocol's execution, it's imperative to acquire a JSON Web Token (JWT) from GitHub's OIDC provider. To request this token, it's essential to configure the specified permission in the workflow file:
```yml
permissions:
id-token: write
id-token: write
```
<div id="workflowstep2"/>

2. **Pass the 'oidc-provider-name' input to the Action (Required)**: The 'oidc-provider-name' parameter designates the OIDC configuration whose one of its identity mapping should align with the generated JWT claims. This input needs to align with the 'Provider Name' value established within the OIDC configuration.
3. **Pass the 'oidc-audience' input to the Action (Optional)**: The 'oidc-audience' input defines the intended recipients of an ID token (JWT), ensuring access is restricted to authorized recipients for the cloud (Artifactory). By default, it contains the URL of the repository owner.
This value, if transmitted, will be used as an argument in core.getIDToken(), which generates the JWT. It enforces a condition, allowing only workflows within the designated repository/organization to access the cloud role. Read more about it [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-audience-value).
```yml
- name: Install JFrog CLI
uses: jfrog/setup-jfrog-cli@v3
env:
JF_URL: ${{ secrets.JF_URL }}
with:
oidc-provider-name: <Provider Name value given in step 1>
oidc-audience: <URL to the intended audience>
```
2. **Pass the 'oidc-provider-name' input to the Action (Required)**: The 'oidc-provider-name' parameter designates the OIDC configuration whose one of its identity mapping should align with the generated JWT claims. This input needs to align with the 'Provider Name' value established within the OIDC configuration in the JFrog Platform.
3. **Pass the 'oidc-audience' input to the Action (Optional)**: The 'oidc-audience' input defines the intended recipients of an ID token (JWT), ensuring access is restricted to authorized recipients for the cloud (Artifactory). By default, it contains the URL of the GitHub repository owner.
This value, if transmitted, will be used as an argument in core.getIDToken(), which generates the JWT. It enforces a condition, allowing only workflows within the designated repository/organization to access the cloud role. Read more about it [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-audience-value).

```yml
- name: Install JFrog CLI
uses: jfrog/setup-jfrog-cli@v3
env:
JF_URL: ${{ secrets.JF_URL }}
with:
oidc-provider-name: <Provider Name value given in step 1>
oidc-audience: <URL to the intended audience>
```

## Setting the build name and build number when publishing build-info to Artifactory
The Action automatically sets the following environment variables:
Expand Down
8 changes: 4 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ inputs:
download-repository:
description: "Remote repository in Artifactory pointing to 'https://releases.jfrog.io/artifactory/jfrog-cli'. Use this parameter in case you don't have an Internet access."
required: false
oidc-audience:
description: "Recipient for which the JWT is intended. By default it contains the URL to the repository owner."
required: false
oidc-provider-name:
description: "Provider Name's value that was set in OpenId Connect integration."
description: "Provider Name's value that was set in OpenId Connect integration in the JFrog platform."
required: false
oidc-audience:
description: "By default, this is the URL of the GitHub repository owner, such as the organization that owns the repository."
required: false

runs:
Expand Down
40 changes: 13 additions & 27 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ class Utils {
static getJfrogCredentials() {
return __awaiter(this, void 0, void 0, function* () {
let jfrogCredentials = this.collectJfrogCredentialsFromEnvVars();
if (!this.shouldUseOpenIDConnect(jfrogCredentials)) {
const oidcProviderName = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME);
if (!oidcProviderName) {
// Use JF_ENV or the credentials found in the environment variables
return jfrogCredentials;
}
core.info('The JFrog platform credentials were not configured. Obtaining an access token through OpenID Connect.');
if (!jfrogCredentials.jfrogUrl) {
throw new Error(`JF_URL must be provided when oidc-provider-name is specified`);
}
core.info('Obtaining an access token through OpenID Connect.');
const audience = core.getInput(Utils.OIDC_AUDIENCE_ARG);
let jsonWebToken;
try {
Expand All @@ -66,34 +70,13 @@ class Utils {
throw new Error(`Getting openID Connect JSON web token failed: ${error.message}`);
}
try {
return yield this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken);
return yield this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken, oidcProviderName);
}
catch (error) {
throw new Error(`Exchanging JSON web token with an access token failed: ${error.message}`);
}
});
}
/**
* Returns true if OpenID Connect authentication should be used.
* @param jfrogCredentials - Credentials retrieved from the environment variables
* @returns true if OpenID Connect authentication should be used
*/
static shouldUseOpenIDConnect(jfrogCredentials) {
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
// To enable OpenIDConnect authentication, users must configure the 'id-token: write' permission, which sets the ACTIONS_ID_TOKEN_REQUEST_URL environment variable.
// If this variable is empty, it indicates that OIDC should not be utilized.
return false;
}
if (!jfrogCredentials.jfrogUrl) {
// If no JFrog URL is specified, we can't use OpenID Connect
return false;
}
if (jfrogCredentials.password || jfrogCredentials.accessToken) {
// If credentials are specified - use them instead
return false;
}
return true;
}
/**
* Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure
* @returns JfrogCredentials struct with all credentials found in environment variables
Expand All @@ -118,20 +101,20 @@ class Utils {
* Exchanges JWT with a valid access token
* @param jfrogCredentials existing JFrog credentials - url, access token, username + password
* @param jsonWebToken JWT achieved from GitHub JWT provider
* @param oidcProviderName OIDC provider name
* @returns an access token for the requested Artifactory server
*/
static getAccessTokenFromJWT(jfrogCredentials, jsonWebToken) {
static getAccessTokenFromJWT(jfrogCredentials, jsonWebToken, oidcProviderName) {
return __awaiter(this, void 0, void 0, function* () {
// If we've reached this stage, the jfrogCredentials.jfrogUrl field should hold a non-empty value obtained from process.env.JF_URL
const exchangeUrl = jfrogCredentials.jfrogUrl.replace(/\/$/, '') + '/access/api/v1/oidc/token';
core.debug('Exchanging JSON web token with an access token');
const providerName = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME, { required: true });
const httpClient = new http_client_1.HttpClient();
const data = `{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"subject_token": "${jsonWebToken}",
"provider_name": "${providerName}"
"provider_name": "${oidcProviderName}"
}`;
const additionalHeaders = {
'Content-Type': 'application/json',
Expand All @@ -143,6 +126,9 @@ class Utils {
if (jfrogCredentials.accessToken) {
core.setSecret(jfrogCredentials.accessToken);
}
if (responseJson.errors) {
throw new Error(`${JSON.stringify(responseJson.errors)}`);
}
return jfrogCredentials;
});
}
Expand Down
46 changes: 18 additions & 28 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ export class Utils {
*/
public static async getJfrogCredentials(): Promise<JfrogCredentials> {
let jfrogCredentials: JfrogCredentials = this.collectJfrogCredentialsFromEnvVars();
if (!this.shouldUseOpenIDConnect(jfrogCredentials)) {
const oidcProviderName: string = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME);
if (!oidcProviderName) {
// Use JF_ENV or the credentials found in the environment variables
return jfrogCredentials;
}

core.info('The JFrog platform credentials were not configured. Obtaining an access token through OpenID Connect.');
if (!jfrogCredentials.jfrogUrl) {
throw new Error(`JF_URL must be provided when oidc-provider-name is specified`);
}
core.info('Obtaining an access token through OpenID Connect.');
const audience: string = core.getInput(Utils.OIDC_AUDIENCE_ARG);
let jsonWebToken: string | undefined;
try {
Expand All @@ -66,34 +70,12 @@ export class Utils {
}

try {
return await this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken);
return await this.getAccessTokenFromJWT(jfrogCredentials, jsonWebToken, oidcProviderName);
} catch (error: any) {
throw new Error(`Exchanging JSON web token with an access token failed: ${error.message}`);
}
}

/**
* Returns true if OpenID Connect authentication should be used.
* @param jfrogCredentials - Credentials retrieved from the environment variables
* @returns true if OpenID Connect authentication should be used
*/
private static shouldUseOpenIDConnect(jfrogCredentials: JfrogCredentials): boolean {
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
// To enable OpenIDConnect authentication, users must configure the 'id-token: write' permission, which sets the ACTIONS_ID_TOKEN_REQUEST_URL environment variable.
// If this variable is empty, it indicates that OIDC should not be utilized.
return false;
}
if (!jfrogCredentials.jfrogUrl) {
// If no JFrog URL is specified, we can't use OpenID Connect
return false;
}
if (jfrogCredentials.password || jfrogCredentials.accessToken) {
// If credentials are specified - use them instead
return false;
}
return true;
}

/**
* Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure
* @returns JfrogCredentials struct with all credentials found in environment variables
Expand All @@ -120,20 +102,24 @@ export class Utils {
* Exchanges JWT with a valid access token
* @param jfrogCredentials existing JFrog credentials - url, access token, username + password
* @param jsonWebToken JWT achieved from GitHub JWT provider
* @param oidcProviderName OIDC provider name
* @returns an access token for the requested Artifactory server
*/
private static async getAccessTokenFromJWT(jfrogCredentials: JfrogCredentials, jsonWebToken: string): Promise<JfrogCredentials> {
private static async getAccessTokenFromJWT(
jfrogCredentials: JfrogCredentials,
jsonWebToken: string,
oidcProviderName: string,
): Promise<JfrogCredentials> {
// If we've reached this stage, the jfrogCredentials.jfrogUrl field should hold a non-empty value obtained from process.env.JF_URL
const exchangeUrl: string = jfrogCredentials.jfrogUrl!.replace(/\/$/, '') + '/access/api/v1/oidc/token';
core.debug('Exchanging JSON web token with an access token');

const providerName: string = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME, { required: true });
const httpClient: HttpClient = new HttpClient();
const data: string = `{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"subject_token": "${jsonWebToken}",
"provider_name": "${providerName}"
"provider_name": "${oidcProviderName}"
}`;

const additionalHeaders: OutgoingHttpHeaders = {
Expand All @@ -147,6 +133,9 @@ export class Utils {
if (jfrogCredentials.accessToken) {
core.setSecret(jfrogCredentials.accessToken);
}
if (responseJson.errors) {
throw new Error(`${JSON.stringify(responseJson.errors)}`);
}
return jfrogCredentials;
}

Expand Down Expand Up @@ -464,4 +453,5 @@ export interface JfrogCredentials {

export interface TokenExchangeResponseData {
access_token: string;
errors: string;
}

0 comments on commit 19cc8f8

Please sign in to comment.