---
url: /en/build/oidc.md
description: >
  Explains how to exchange an id_token via OIDC in Cloud Native Build pipelines
  to integrate with cloud services that support OIDC federation (such as Tencent
  Cloud) without long-lived secrets, and how your own services can validate the
  id_token issued by CNB.
---
Cloud Native Build can issue an OIDC `id_token` in a pipeline to prove
"the identity of this build" to services that support OIDC federation
(cloud providers or self-hosted services), and exchange it for short-lived credentials
so that no long-lived secrets need to be stored in the repository.

## Why Use OIDC

Traditionally, you store a cloud provider's `SecretId` / `SecretKey` as a repository
[secret](../repo/secret.md), which means secrets are centralized, widely exposed, and hard to rotate.
With OIDC:

* **No long-lived secrets**: no cloud credentials stored in the repository;
  the pipeline requests short-lived credentials on demand.
* **Authentication separated from authorization**: who can obtain which permissions is controlled
  by the trust policy and permission policy on the target service side,
  down to repository, branch, and event granularity.
* **Automatic credential rotation**: the `id_token` is valid for 5 minutes and the exchanged
  credentials expire automatically, so no manual rotation is needed.

## How It Works

```text
1. One-time setup: register CNB as a trusted OIDC identity provider on the target service side
   and configure its trust policy.
2. At pipeline runtime: call the CNB OpenAPI /{repo}/-/id_token with $CNB_TOKEN to get an id_token (JWT).
3. A plugin or script passes the id_token to the cloud provider
   (for example, Tencent Cloud STS AssumeRoleWithWebIdentity) or to your own service.
4. The target service verifies the signature using the CNB JWKS public keys
   and validates the aud / sub / exp claims.
5. Once verified, short-lived credentials are issued, or business data is returned.
```

Standard endpoints provided by CNB:

| Endpoint | Description |
|----------|-------------|
| `/.well-known/openid-configuration` | OIDC discovery document, including `issuer` and `jwks_uri` |
| `/.well-known/jwks.json` | Signing public key set (RS256) used by the target service to verify signatures |

The issuer of the site is the instance address `https://cnb.cool`, and the endpoints are:

* Discovery document: `https://cnb.cool/.well-known/openid-configuration`
* Signing public key set (JWKS): `https://cnb.cool/.well-known/jwks.json`

## Prerequisites

* OIDC is enabled on the instance.
* The pipeline is triggered by an allowed event (see the table below). For events such as pull requests,
  issues, and scheduled tasks, the ref is not trustworthy or there is no token-exchange scenario,
  so the platform rejects the request.
* The user who triggers the pipeline has [developer](../guide/role-permissions.md) permission or above.
* The `aud` in the request is registered on the instance. Audiences are created by an administrator
  under **OIDC Settings - Audience** in the admin console; use `sts.tencentcloudapi.com`
  for Tencent Cloud.

### Events Allowed to Issue Tokens

| Event | Trigger Source | Permission Requirement | `sub` Example |
|-------|----------------|------------------------|---------------|
| `push` | Git push (issued by the platform) | — | `my-group/my-repo:main:push` |
| `tag_push` | Tag push (issued by the platform) | — | `my-group/my-repo:v1.0:tag_push` |
| `tag_deploy.{env}` | Deployment from the UI / API | Push code + push tag permission | `my-group/my-repo:v1.0:tag_deploy.prod` |
| `api_trigger[_{name}]` | OpenAPI | Repository write permission | `my-group/my-repo:main:api_trigger` |
| `web_trigger[_{name}]` | UI button | Repository write permission | `my-group/my-repo:main:web_trigger` |

## id\_token Claims

The `id_token` is an RS256-signed JWT, for example:

```json
{
  "iss": "https://cnb.cool",
  "aud": "sts.tencentcloudapi.com",
  "sub": "my-group/my-repo:main:push",
  "iat": 1692000000,
  "exp": 1692000300,
  "jti": "b1f0e2a4-8c3d-4f6a-9e7b-2c5d8a1f3e09",
  "repository": "my-group/my-repo",
  "ref": "main",
  "event_name": "push",
  "actor": "robin",
  "run_id": "5368227742",
  "sha": "abc123..."
}
```

| Claim | Description |
|-------|-------------|
| `iss` | Issuer, the public HTTPS address of the instance |
| `aud` | Audience, the `aud` passed in when requesting the `id_token` |
| `sub` | Subject, in the three-segment form `{slug}:{ref}:{event}`, generated by the server |
| `iat` / `exp` | Issued-at / expiration time. The `id_token` is valid for 5 minutes |
| `jti` | Unique token ID, which can be used for replay protection |
| `repository` | Full repository path, same as `CNB_REPO_SLUG` |
| `ref` | Branch or tag name (short name, without the `refs/` prefix) |
| `event_name` | Triggering event name |
| `actor` | Username of the user who triggered the pipeline |
| `run_id` | Pipeline ID |
| `sha` | SHA of the current commit |

### Three-Segment sub

| Segment | Meaning | Example |
|---------|---------|---------|
| `{slug}` | Full repository path, same as `CNB_REPO_SLUG` | `my-group/my-repo` |
| `{ref}` | Branch or tag name (short name, without the `refs/` prefix) | `main` / `v1.0` |
| `{event}` | Triggering event name | `push` / `tag_push` / `tag_deploy.prod` |

Because `sub` already contains the repository, ref, and event dimensions, the target service only needs
to match `sub` to enforce branch-level and event-level authorization.
All segments are generated by the server based on the pipeline context and cannot be overridden by the caller.

:::: warning
`aud` only identifies "who the credential is for". All `id_token`s issued under the same audience have
an identical `aud`, which **cannot** distinguish which repository or branch initiated the request.
The trust policy of the target service **must** also validate `sub`; otherwise you are granting
permissions to any repository on the instance.
::::

## Requesting the id\_token in a Pipeline

### Endpoint

```text
POST {CNB_API_ENDPOINT}/{repo}/-/id_token
Authorization: Bearer $CNB_TOKEN
Content-Type: application/json

{"aud":"sts.tencentcloudapi.com"}
```

Only `aud` may be passed in the request body. The `sub` claim and all other claims are generated by the
server from the pipeline context and cannot be overridden by the caller.

Response:

```json
{
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}
```

:::: tip
The response does **not** return `expires_in`. The actual validity period of the `id_token` is determined
by the `exp` claim inside the JWT, so parse `exp` to determine the remaining lifetime.
::::

### Example

```yaml title=".cnb.yml"
main:
  push:
    - stages:
        - name: Request id_token
          image: alpine
          script: |
            apk add --no-cache curl jq
            ID_TOKEN=$(curl -sS -X POST "$CNB_API_ENDPOINT/$CNB_REPO_SLUG/-/id_token" \
              -H "Authorization: Bearer $CNB_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"aud":"sts.tencentcloudapi.com"}' | jq -r '.id_token')
            # Never print ID_TOKEN to the build log
```

A single pipeline can request at most 10 `id_token`s per minute. Exceeding the limit returns 429.

## Integrating with Tencent Cloud

Use the
[tencentcloud-oidc-auth](https://cnb.cool/cnb/plugins/tencentcom/tencentcloud-oidc-auth) plugin
to obtain Tencent Cloud CAM temporary credentials in a pipeline without configuring
`SecretId` / `SecretKey` in the repository.

### Step 1: Configure Tencent Cloud (one-time)

#### 1. Create an OIDC identity provider

Open [Create Identity Provider](https://console.cloud.tencent.com/cam/idp/create)
(**CAM Console → Identity Provider → Role SSO → Create Identity Provider**)
and fill in the form as follows:

| Field | Value |
|-------|-------|
| Provider type | Select `OIDC` |
| Identity provider name | A domain name (for example `cnb.example.com`) is recommended so that multiple CNB instances under the same CAM account can be told apart. This value is the provider identifier and must be consistent with `oidc-provider/<name>` in the trust policy below and with the plugin's `provider_id` parameter |
| Identity provider URL | The CNB issuer, which must match the `iss` of the `id_token` exactly — `https://cnb.cool` |
| Client ID | `sts.tencentcloudapi.com` |
| Identity provider public key | Open this site's JWKS endpoint `https://cnb.cool/.well-known/jwks.json` and paste the public key content here |

#### 2. Create a CAM role and configure its trust policy

Create an identity-provider role, select the provider created above, and use the following trust policy:

```json
{
  "version": "2.0",
  "statement": [
    {
      "effect": "allow",
      "action": ["sts:AssumeRoleWithWebIdentity"],
      "principal": {
        "federated": [
          "qcs::cam::uin/<ACCOUNT_ID>:oidc-provider/cnb.example.com"
        ]
      },
      "condition": {
        "string_equal": {
          "oidc:aud": "sts.tencentcloudapi.com",
          "oidc:sub": "my-group/my-repo:main:push"
        }
      }
    }
  ]
}
```

From coarse to fine granularity:

* Repository level (minimum requirement): `"oidc:sub": "my-group/my-repo:*"` with `string_like`
* Branch level: `"oidc:sub": "my-group/my-repo:main:*"` with `string_like`
* Exact match: `"oidc:sub": "my-group/my-repo:main:push"` with `string_equal`

To authorize multiple branches or events, add multiple entries to the `statement` array,
or use `string_like` wildcards on the branch segment (for example `my-group/my-repo:release-*:push`).

:::: danger
The trust policy **must** include an `oidc:sub` condition, and it must be **at least precise to the
repository level** (`{slug}`).

`oidc:aud` only verifies that the audience is `sts.tencentcloudapi.com`, which is identical for every
CNB pipeline and cannot identify the issuing subject. Without the `oidc:sub` restriction, any CNB user
can use an `id_token` issued by any of their own repositories to assume your role and obtain all cloud
resource permissions granted to that role. Never use a bare `%` wildcard.
::::

#### 3. Attach permission policies to the role

Follow the principle of least privilege, for example `QcloudCOSDataWriteOnly`, `QcloudCDNPushOnly`,
or a custom policy.

### Step 2: Configure the pipeline

```yaml title=".cnb.yml"
main:
  push:
    - stages:
        - name: Tencent Cloud OIDC auth
          image: tencentcom/tencentcloud-oidc-auth
          settings:
            role_arn: qcs::cam::uin/123456789:roleName/cnb-deploy
            provider_id: cnb.example.com
            region: ap-guangzhou

        - name: Prepare Python env
          image: python:3-slim
          script: |
            python -m venv "$CNB_BUILD_WORKSPACE/.venv"
            "$CNB_BUILD_WORKSPACE/.venv/bin/pip" install -q --disable-pip-version-check tccli

        - name: Verify credentials
          image: python:3-slim
          script: |
            . "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
            "$CNB_BUILD_WORKSPACE/.venv/bin/tccli" sts GetCallerIdentity

        - name: Deploy
          image: python:3-slim
          script: |
            . "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
            "$CNB_BUILD_WORKSPACE/.venv/bin/tccli" cos PutBucket --Bucket my-bucket --Region "$TENCENTCLOUD_REGION"

        - name: coscli (example)
          image: tencentcom/coscli
          script: |
            . "$CNB_BUILD_WORKSPACE/.tencentcloud-credentials"
            coscli ls cos://my-bucket --init-skip \
              -e "cos.${TENCENTCLOUD_REGION}.myqcloud.com" \
              -i "$TENCENTCLOUD_SECRET_ID" \
              -k "$TENCENTCLOUD_SECRET_KEY" \
              --token "$TENCENTCLOUD_TOKEN" \
              --limit 10
```

::::: tip
Unlike tccli / the SDK, `coscli` **does not read the `TENCENTCLOUD_*` environment variables** — pass the
credentials explicitly with `-i` / `-k` / `--token` and use `--init-skip` to skip interactive
initialization. The `cos://` target must be a full bucket name (including the APPID).
`ls` requires bucket-level permissions (`cos:HeadBucket` / `cos:GetBucket`); granting only
object-level permissions (such as `cos:GetObject`) makes `ls` return 403.
:::::

### Plugin Parameters

| Parameter | Required | Default | Description |
|-----------|:--------:|---------|-------------|
| `role_arn` | Yes | — | ARN of the CAM role to assume, must match `qcs::cam::uin/<ACCOUNT_ID>:roleName/<ROLE_NAME>`; a malformed value fails fast at plugin startup |
| `provider_id` | Yes | — | Identifier of the OIDC provider in Tencent Cloud, that is, the "Identity provider name" in the console. It must match `oidc-provider/<name>` in the trust policy (the example here uses `cnb.example.com`) |
| `region` | Yes | — | Required Tencent Cloud API 3.0 common parameter (STS request region), also written to `TENCENTCLOUD_REGION` |
| `role_session_name` | No | `cnb-${CNB_BUILD_USER}` | STS session name used for cloud audit; falls back to `cnb` when there is no triggerer, auto-truncated beyond 64 characters |
| `duration_seconds` | No | `3600` | Credential validity in seconds, clamped to `[900, 7200]` |
| `audience` | No | `sts.tencentcloudapi.com` | `id_token` audience, must match the Client ID of the Tencent Cloud provider |
| `cnb_api` | No | `$CNB_API_ENDPOINT` | CNB API address (override), must start with `https://`; plaintext `http://` is rejected |

### Credentials File

The plugin writes temporary credentials to `.tencentcloud-credentials` in the workspace with
permission `0600`. The file contains shell-safe `export` statements (values wrapped in single quotes),
and subsequent jobs load them with `source`:

| Variable | Description |
|----------|-------------|
| `TENCENTCLOUD_SECRET_ID` | Temporary SecretId |
| `TENCENTCLOUD_SECRET_KEY` | Temporary SecretKey |
| `TENCENTCLOUD_TOKEN` | Temporary token |
| `TENCENTCLOUD_SECURITY_TOKEN` | Same as the token, for tools such as Pulumi |
| `TENCENTCLOUD_REGION` | Region |

File content example:

```bash
export TENCENTCLOUD_SECRET_ID='AKID...'
export TENCENTCLOUD_SECRET_KEY='...'
export TENCENTCLOUD_TOKEN='...'
export TENCENTCLOUD_SECURITY_TOKEN='...'
export TENCENTCLOUD_REGION='ap-guangzhou'
```

Each plugin job exchanges only one set of credentials. For multi-account scenarios, configure multiple
plugin jobs with different `role_arn` values.

### Troubleshooting

| Error | Cause and Fix |
|-------|---------------|
| `CNB_TOKEN is empty` | Not running in a pipeline environment (no token when debugging locally) |
| `403: event not allowed for oidc id_token` | The current event is not allowed; trigger with push / tag events instead |
| `403: pipeline token missing ref context` | The pipeline token lacks ref metadata; check the build service version |
| `403: audience not allowed` | The `aud` is not registered on the instance; ask an administrator to create it |
| STS `UnauthorizedOperation` | The `oidc:sub` in the role trust policy does not match the current pipeline; compare it with the actual `sub` printed in the plugin log |
| STS `InvalidParameter.WebIdentityTokenError` | The Provider URL / Client ID in Tencent Cloud do not match the CNB issuer / audience |
| STS `InvalidParameter.ValueTooLarge` | `duration_seconds` exceeds 7200; the plugin already clamps it to 7200, check for other callers |
| `PLUGIN_ROLE_ARN is malformed` | `role_arn` does not match `qcs::cam::uin/<ACCOUNT_ID>:roleName/<ROLE_NAME>`; the plugin rejects it at startup |
| `CNB API endpoint must use https://` | `cnb_api` uses `http://`; the plugin rejects plaintext transmission of `CNB_TOKEN` |

## Integrating with Your Own Service

If the target is your own release system, configuration center, or internal API, the pipeline can pass
the `id_token` as a Bearer token. After validating it with the standard OIDC flow, the server can trust
the identity of this build without issuing a dedicated long-lived API key for the pipeline.

### Step 1: Register the audience

An administrator creates the audience under **OIDC Settings - Audience** in the admin console.
The value must be a domain name (for example `deploy.example.com`) and no longer than 128 characters.
This value becomes the `aud` claim of the `id_token` and the basis for server-side validation.

### Step 2: Request and pass the id\_token in the pipeline

```yaml title=".cnb.yml"
main:
  push:
    - stages:
        - name: Call the internal deploy service
          image: curlimages/curl
          script: |
            ID_TOKEN=$(curl -sS -X POST "$CNB_API_ENDPOINT/$CNB_REPO_SLUG/-/id_token" \
              -H "Authorization: Bearer $CNB_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"aud":"deploy.example.com"}' | jq -r '.id_token')
            curl -sS -X POST https://deploy.example.com/api/deploy \
              -H "Authorization: Bearer $ID_TOKEN" \
              -d "version=$CNB_BRANCH"
```

### Step 3: Validate the id\_token on the server

The server must complete all of the following checks:

1. **Fetch public keys**: read `jwks_uri` from `{iss}/.well-known/openid-configuration`,
   then fetch the JWKS key set. Keys are rotated, so match by `kid`, cache the keys, and refresh periodically.
2. **Verify the signature**: verify the RS256 signature with the public key matching `kid`,
   and allow only `RS256` in the algorithm allowlist.
3. **Validate standard claims**:
   * `iss` equals the expected CNB instance address
   * `aud` equals the audience registered for this service
   * `exp` has not passed, and validate `nbf` when necessary
4. **Validate `sub`**: compare it against the allowlist of repositories / branches / events,
   **at least precise to the repository level**.
5. **Replay protection (optional)**: record `jti` and reject reuse.

:::: warning
Do not skip the `aud` and `sub` validation, and do not make authorization decisions based on claims such
as `repository` or `ref` before the signature is verified — those claims are only trustworthy
after signature verification succeeds.
::::

### Example: Node.js (using jose)

```js
import express from 'express'
import { createRemoteJWKSet, jwtVerify } from 'jose'

const ISSUER = 'https://cnb.cool'
const AUDIENCE = 'deploy.example.com'
// Only allow push events on the main branch of my-group/my-repo
const ALLOWED_SUBS = [/^my-group\/my-repo:main:push$/]

const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`))

async function verifyRequest(req) {
  const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, '')
  if (!token) throw new Error('missing bearer token')

  // Verify signature + iss + aud + exp, restricted to RS256
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ['RS256'],
  })

  // Validate sub, at least precise to the repository level
  if (!ALLOWED_SUBS.some((re) => re.test(payload.sub))) {
    throw new Error(`sub not allowed: ${payload.sub}`)
  }
  return payload
}

const app = express()
app.post('/api/deploy', async (req, res) => {
  try {
    const claims = await verifyRequest(req)
    res.json({ ok: true, repository: claims.repository, ref: claims.ref })
  } catch (err) {
    res.status(401).json({ ok: false, message: err.message })
  }
})
app.listen(3000)
```

Other languages can use their own OIDC libraries, such as `github.com/coreos/go-oidc` for Go,
`pyjwt` with `jwcrypto` for Python, and `spring-security-oauth2-jose` for Java.

## Security Recommendations

* **Always validate `sub`**: the trust policy of the target service must at least restrict it to the
  repository level, and preferably to the branch and event level.
* **Least privilege**: create a separate role for each environment (dev / staging / prod)
  and attach only the required policies.
* **Avoid credential leaks**: never print the `id_token` or temporary credentials to the build log,
  and never pass them through `set-output`. Use masked `set-secret` or files for cross-job passing.
* **Prefer dedicated plugins**: for cloud providers, use official or community plugins instead of
  handling credentials yourself.
* **The plugin keeps credentials out of logs**: `tencentcloud-oidc-auth` prints only the first 8
  characters of the credential sha256 hash plus the expiration time, and passes credentials through a
  workspace file (`0600`) instead of `set-output`.
* **CNB API HTTPS is enforced**: the plugin parameter `cnb_api` must use `https://`; the plugin rejects
  `http://` so that `CNB_TOKEN` and the `id_token` are never sent in plaintext. Never point `cnb_api` at
  an untrusted address.
* **Pre-validate after exchange**: right after obtaining an `id_token`, the plugin checks that `aud`
  matches the expected value and that `exp` has not passed, keeping bad tokens away from STS. Do the
  same two checks if you implement the exchange yourself.
* **Mind the event allowlist**: only `push`, `tag_push`, `tag_deploy`, `api_trigger`, and `web_trigger`
  events can issue an `id_token`. Custom event names must not contain `:`, otherwise issuance is rejected.

## Limitations

* Only instances with OIDC enabled are supported.
* The `id_token` is valid for a fixed 5 minutes; complete the token exchange within that window.
* A single pipeline can request at most 10 `id_token`s per minute.
* The `aud` must be registered in advance by a platform administrator and must be a domain name.
* Pipelines triggered by pull request, issue, or scheduled events cannot obtain an `id_token`.
