# Authenticate with OIDC (/docs/oidc)

GitHub Actions, Vercel, GitLab, CircleCI and most other CI and hosting platforms give each job or
function a short-lived, signed **OIDC ID token** that says who it is: which repository, branch,
project or environment. An environment's **OIDC credential** trusts those tokens under conditions
you set, and exchanges a matching one for a **coss.com access token** that works like an API key
for an hour at most. Nothing long-lived is stored in your CI or hosting settings, so there is no
key to leak or rotate.

## Create a credential [#create-a-credential]

On the environment's **API keys** page, under **OIDC credentials**, choose **Add OIDC credential**
(owners and admins). Pick the provider and say what to trust:

| Provider       | Issuer                                       | You enter                                                       | Pinned on first use                   |
| -------------- | -------------------------------------------- | --------------------------------------------------------------- | ------------------------------------- |
| GitHub Actions | `https://token.actions.githubusercontent.com` | Repository, and optionally an environment, a ref, a workflow    | `repository_id`, `repository_owner_id` |
| Vercel         | `https://oidc.vercel.com/<team>`             | Team, project, and optionally the Vercel environment            | `owner_id`, `project_id`              |
| GitLab         | `https://gitlab.com` or your own GitLab      | Project path, and optionally a ref and an environment           | `project_id`, `namespace_id`          |
| CircleCI       | `https://oidc.circleci.com/org/<org-id>`     | Organization and project ids, and optionally a context          | —                                     |
| Custom issuer  | Any public `https://` issuer                 | The audience and conditions on any claims (`claim = value`, `claim ~ pattern`) | —                    |

A credential is an organization key without a secret: choose its access (full, sending only, read
only or custom) like a key's, and how long its access tokens last (5 minutes to an hour). Its
spend, budget, logs and audit events are the credential's, by its name.

**Pinning.** A repository or project name can be deleted and registered again by someone else.
So the first token exchanged records the provider's ids for it, and later tokens must carry the
same ids. After moving a repository on purpose, **Reset pins** on the credential.

Each credential has an id (`oidc_…`). It isn't a secret: keep it in your workflow file or project
settings.

## Use it [#use-it]

The SDK, the CLI and the MCP server use `COSS_OIDC_CREDENTIAL_ID` when no `COSS_API_KEY` is set.
They find the job's ID token on their own: `COSS_OIDC_TOKEN` if set, otherwise GitHub Actions'
token endpoint, Vercel's (the function's request, else `VERCEL_OIDC_TOKEN`) or CircleCI's
(`CIRCLE_OIDC_TOKEN_V2`).

```ts twoslash
import { Coss } from "coss";

// Exchanges the ID token on the first request, and again before the access token expires.
const coss = new Coss({ oidc: { credentialId: "oidc_01k7c8m4q2f3e9v6t5r1n0p8yz" } });

const { data, error } = await coss.email.send({
  from: "Acme <deploys@acme.com>",
  to: "team@acme.com",
  subject: "Deployed",
  text: "main is live.",
});
```

Pass `token` to read the ID token yourself (`token: () => readToken()`), and `audience` when
GitHub Actions should issue it for another audience than `https://coss.com`.

### GitHub Actions [#github-actions]

The job needs `permissions: id-token: write`:

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    env:
      COSS_OIDC_CREDENTIAL_ID: oidc_01k7c8m4q2f3e9v6t5r1n0p8yz
    steps:
      - run: npx coss-cli emails send --from deploys@acme.com --to team@acme.com --subject "Deployed" --text "main is live."
```

### Vercel [#vercel]

Turn on OIDC Federation in the project's settings (the issuer mode you pick there must match the
credential's), then set `COSS_OIDC_CREDENTIAL_ID`. Functions get a fresh token with each request
and builds get `VERCEL_OIDC_TOKEN`; the SDK reads both.

### GitLab [#gitlab]

Ask for an ID token for `https://coss.com` as `COSS_OIDC_TOKEN`:

```yaml
deploy:
  id_tokens:
    COSS_OIDC_TOKEN:
      aud: https://coss.com
  variables:
    COSS_OIDC_CREDENTIAL_ID: oidc_01k7c8m4q2f3e9v6t5r1n0p8yz
  script:
    - npx coss-cli emails send --from deploys@acme.com --to team@acme.com --subject "Deployed" --text "main is live."
```

### Anything else [#anything-else]

Exchange the token yourself with [`POST /v1/oidc/token`](/docs/api-reference/environment/exchange-oidc-token)
(no API key needed) and use the `access_token` as a bearer key:

```bash
curl -X POST https://api.coss.com/v1/oidc/token \
  -H "Content-Type: application/json" \
  -d '{"credential_id": "oidc_01k7c8m4q2f3e9v6t5r1n0p8yz", "token": "'"$ID_TOKEN"'"}'
```

`DELETE /v1/api_key` with an access token revokes just that token, for a job that wants to sign
out when it's done. Deleting the credential revokes all of its tokens at once.

## When a token is refused [#when-a-token-is-refused]

Every refusal answers `401` `invalid_oidc_token`, whatever the reason, so a token from someone
else's repository learns nothing about your conditions. The reason is in the environment's
[logs](/docs/logs): the request line of `POST /v1/oidc/token` carries `auth_failure`, such as
`audience_mismatch`, `condition_failed:repository`, `pinned_claim_mismatch:repository_id`,
`expired` or `issuer_unreachable`.

coss.com verifies each token's signature against the issuer's published keys (RSA, ECDSA or
EdDSA only), its exact issuer and audience, its expiry, then every condition and pinned claim.
