Skip to content

Commit

Permalink
Merge pull request #20 from DopplerHQ/rgharris/oidc
Browse files Browse the repository at this point in the history
New OIDC auth method
  • Loading branch information
rgharris authored Jan 21, 2025
2 parents 8ac64b8 + 47ddb19 commit cd2efbf
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 26 deletions.
55 changes: 41 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,62 @@ This action enables you to fetch Doppler secrets for use in your GitHub Actions.

The action can be configured in two ways:

* Service Token (recommended)
* Service Account Token with Project and Config
* Service Account with Project and Config via either:
- Service Account Identity via OIDC (recommended)
- Service Account Token
* Service Token

### Service Token
### Service Account

A [Doppler Service Token](https://docs.doppler.com/docs/service-tokens) provides read-only access to a single config and is recommended due to its limited access scope.
A Doppler Service Account allows for a configurable set of permissions to services in your workplace. A project and config must be specified when using a service account. Your workplace must be on the Team or Enterprise plan in order to use service accounts.

Create a GitHub repository secret named `DOPPLER_TOKEN` or if using multiple Service Tokens (e.g. for a Monorepo), you can prefix the secret name using with application name, e.g. `AUTH_API_DOPPLER_TOKEN`.
#### Service Account Identity via OIDC

Then supply the Service Token using the `doppler-token` input:
[Identities](https://docs.doppler.com/docs/service-account-identities) allow a service account to authenticate to Doppler via OIDC without using a static API token. This method works like the Service Account Token method below but without a static API token.

The `auth-method`, `doppler-identity-id`, `doppler-project` and `doppler-config` inputs must be provided when using a Service Account Identity. The permission `id-token: write` is required so that Doppler can obtain an OIDC token from Github for authentication.

```yaml
jobs:
your-example-job:
permissions:
id-token: write # required for obtaining the OIDC JWT from Github
steps:
- uses: dopplerhq/[email protected]
id: doppler
with:
auth-method: oidc
doppler-identity-id: <your-service-account-identity-uuid>
doppler-project: auth-api
doppler-config: ci-cd
```
#### Service Account Token
The `doppler-project` and `doppler-config` inputs must be provided when using a Service Account Token:

```yaml
- uses: dopplerhq/secrets-fetch-action@v1.2.0
- uses: dopplerhq/secrets-fetch-action@v1.3.0
id: doppler
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
doppler-project: auth-api
doppler-config: ci-cd
```

### Service Account Token
### Service Token

A [Doppler Service Token](https://docs.doppler.com/docs/service-tokens) provides read-only access to a single config.

A Doppler Service Account Token allows for a configurable set of permissions to services in your workplace. The `doppler-project` and `doppler-config` inputs must be provided when using a Service Account Token:
Create a GitHub repository secret named `DOPPLER_TOKEN` or if using multiple Service Tokens (e.g. for a Monorepo), you can prefix the secret name using with application name, e.g. `AUTH_API_DOPPLER_TOKEN`.

Then supply the Service Token using the `doppler-token` input:

```yaml
- uses: dopplerhq/secrets-fetch-action@v1.2.0
- uses: dopplerhq/secrets-fetch-action@v1.3.0
id: doppler
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
doppler-project: auth-api
doppler-config: ci-cd
```

## Usage
Expand All @@ -59,7 +86,7 @@ jobs:
secrets-fetch:
runs-on: ubuntu-latest
steps:
- uses: dopplerhq/secrets-fetch-action@v1.2.0
- uses: dopplerhq/secrets-fetch-action@v1.3.0
id: doppler
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
Expand All @@ -82,7 +109,7 @@ jobs:
secrets-fetch:
runs-on: ubuntu-latest
steps:
- uses: dopplerhq/secrets-fetch-action@v1.2.0
- uses: dopplerhq/secrets-fetch-action@v1.3.0
id: doppler
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
Expand Down
15 changes: 14 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ branding:
icon: 'lock'
color: 'blue'
inputs:
auth-method:
description: >-
Auth method to use.
- "token" (default): Authenticate with a static API token (`doppler-token`)
- "oidc": Authenticate via OIDC. Note that this requires the `id-token: write` permission on the Github job or workflow. See https://docs.doppler.com/docs/service-account-identities
default: "token"
doppler-token:
description: >-
Doppler Service Token that grants access to a single Config within a Project.
See https://docs.doppler.com/docs/service-tokens
required: true
required: false
doppler-project:
description: >-
Doppler Project
Expand All @@ -21,6 +27,13 @@ inputs:
description: >-
Inject secrets as environment variables for subsequent steps if set to `true`.
required: false
doppler-identity-id:
description: >-
Identity to use, required when auth-method is "oidc".
required: false
doppler-api-domain:
default: "api.doppler.com"
required: false
runs:
using: 'node20'
main: 'index.js'
66 changes: 62 additions & 4 deletions doppler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { VERSION } from "./meta.js";
* @param {string} dopplerToken
* @param {string | null} [dopplerProject]
* @param {string | null} [dopplerConfig]
* @returns {() => Promise<Record<string, Record>>}
* @param {string} apiDomain
* @returns {Promise<Record<string, Record>>}
*/
async function fetch(dopplerToken, dopplerProject, dopplerConfig) {
export async function fetch(dopplerToken, dopplerProject, dopplerConfig, apiDomain) {
return new Promise(function (resolve, reject) {
const encodedAuthData = Buffer.from(`${dopplerToken}:`).toString("base64");
const authHeader = `Basic ${encodedAuthData}`;
const userAgent = `secrets-fetch-github-action/${VERSION}`;

const url = new URL("https://api.doppler.com/v3/configs/config/secrets");
const url = new URL(`https://${apiDomain}/v3/configs/config/secrets`);
if (dopplerProject && dopplerConfig) {
url.searchParams.append("project", dopplerProject);
url.searchParams.append("config", dopplerConfig);
Expand Down Expand Up @@ -54,4 +55,61 @@ async function fetch(dopplerToken, dopplerProject, dopplerConfig) {
});
}

export default fetch;
/**
* Exchange an OIDC token for a short lived Doppler service account token
* @param {string} identityId
* @param {string} oidcToken
* @param {string} apiDomain
* @returns {Promise<string>}
*/
export async function oidcAuth(identityId, oidcToken, apiDomain) {
return new Promise(function (resolve, reject) {
const userAgent = `secrets-fetch-github-action/${VERSION}`;

const url = new URL(`https://${apiDomain}/v3/auth/oidc`);
const body = JSON.stringify({
identity: identityId,
token: oidcToken
});

const request = https
.request(
url.href,
{
headers: {
"user-agent": userAgent,
"accepts": "application/json",
"Content-Type": "application/json",
"Content-Length": body.length,
},
method: 'POST'
},
(res) => {
let payload = "";
res.on("data", (data) => (payload += data));
res.on("end", () => {
if (res.statusCode === 200) {
resolve(JSON.parse(payload).token);
} else {
try {
const error = JSON.parse(payload).messages.join(" ");
reject(new Error(`Doppler API Error: ${error}`));
} catch (error) {
// In the event an upstream issue occurs and no JSON payload is supplied
reject(new Error(`Doppler API Error: ${res.statusCode} ${res.statusMessage}`));
}
}
});
}
);

request
.on("error", (error) => {
reject(new Error(`Doppler API Error: ${error}`));
});

request.write(body);

request.end()
});
}
25 changes: 21 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import core from "@actions/core";
import fetch from "./doppler.js";
import { fetch, oidcAuth } from "./doppler.js";

// For local testing
if (process.env.NODE_ENV === "development" && process.env.DOPPLER_TOKEN) {
process.env["INPUT_AUTH-METHOD"] = "token";
process.env["INPUT_DOPPLER-API-DOMAIN"] = "api.doppler.com";
process.env["INPUT_DOPPLER-TOKEN"] = process.env.DOPPLER_TOKEN;
process.env["INPUT_DOPPLER-PROJECT"] = process.env.DOPPLER_PROJECT;
process.env["INPUT_DOPPLER-CONFIG"] = process.env.DOPPLER_CONFIG;
}

const AUTH_METHOD = core.getInput("auth-method");
const API_DOMAIN = core.getInput("doppler-api-domain");
let DOPPLER_TOKEN = "";

if (AUTH_METHOD === "oidc") {
const DOPPLER_IDENTITY_ID = core.getInput("doppler-identity-id", { required: true });
const oidcToken = await core.getIDToken();
core.setSecret(oidcToken);
DOPPLER_TOKEN = await oidcAuth(DOPPLER_IDENTITY_ID, oidcToken, API_DOMAIN);
} else if (AUTH_METHOD === "token") {
DOPPLER_TOKEN = core.getInput("doppler-token", { required: true });
} else {
core.setFailed("Unsupported auth-method");
process.exit();
}

const DOPPLER_META = ["DOPPLER_PROJECT", "DOPPLER_CONFIG", "DOPPLER_ENVIRONMENT"];
const DOPPLER_TOKEN = core.getInput("doppler-token", { required: true });
core.setSecret(DOPPLER_TOKEN);

const IS_SA_TOKEN = DOPPLER_TOKEN.startsWith("dp.sa.");
const IS_SA_TOKEN = DOPPLER_TOKEN.startsWith("dp.sa.") || DOPPLER_TOKEN.startsWith("dp.said.");
const IS_PERSONAL_TOKEN = DOPPLER_TOKEN.startsWith("dp.pt.");
const DOPPLER_PROJECT = (IS_SA_TOKEN || IS_PERSONAL_TOKEN) ? core.getInput("doppler-project") : null;
const DOPPLER_CONFIG = (IS_SA_TOKEN || IS_PERSONAL_TOKEN) ? core.getInput("doppler-config") : null;
Expand All @@ -25,7 +42,7 @@ if (IS_SA_TOKEN && !(DOPPLER_PROJECT && DOPPLER_CONFIG)) {
process.exit();
}

const secrets = await fetch(DOPPLER_TOKEN, DOPPLER_PROJECT, DOPPLER_CONFIG);
const secrets = await fetch(DOPPLER_TOKEN, DOPPLER_PROJECT, DOPPLER_CONFIG, API_DOMAIN);

for (const [key, secret] of Object.entries(secrets)) {
const value = secret.computed || "";
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doppler-secrets-fetch-github-action",
"version": "1.1.3",
"version": "1.3.0",
"description": "GitHub Action to fetch secrets from Doppler's API",
"author": "Doppler",
"license": "Apache-2.0",
Expand Down

0 comments on commit cd2efbf

Please sign in to comment.