> ## Documentation Index
> Fetch the complete documentation index at: https://code.storage/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How to create a key, store its private key, and sign JWTs with the scopes and claims that each operation needs.

Every HTTP request and every Git request carries a [JSON Web Token](https://www.jwt.io/introduction)
(JWT). Your private key signs the token with ES256, ES384, ES512, or RS256. Code Storage checks the
signature with the public key that you registered. The private key never leaves your systems.

<span id="create-an-api-key" />

## Create a key

A key is a key pair that you register with your organization. You keep the private key and use it to
sign tokens. Code Storage stores only the public key.

Open the **Keys** page in your organization dashboard and select **Create key**.

New keys are unrestricted by default. To create a [restricted key](#restricted-keys), enable **Limit
scope for this key**. Deselect the scopes that the key does not need. If you leave every scope
selected, the dashboard saves an unrestricted key.

Your browser creates an ECDSA P-256 key pair and registers the public key. Copy the private key and
your organization name.

<Warning>
  Copy the private key now. We cannot show it again. Anyone with the private key can sign tokens for
  your organization. Never commit it.
</Warning>

## Organization name

The organization name is the token's `iss` claim, which Code Storage uses to find your registered
public keys. It is usually also the subdomain of the API host `api.your-org.code.storage` and the
Git host `your-org.code.storage`. The **Settings** page in the dashboard shows the exact API base
URL and Git host for your organization.

## Store the private key

Store the private key in PKCS8 PEM format on your server or in a secret manager. The SDK clients and
the examples on this page accept this format.

Set `PIERRE_PRIVATE_KEY` to the PEM contents in your local environment or secret manager. The
examples read the key from this variable.

Set `PIERRE_ORG` to your organization name:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
export PIERRE_ORG="your-org"
export PIERRE_PRIVATE_KEY="$(cat /path/to/private-key.pem)"
```

<span id="jwts" />

## Token claims

Clients send the token with requests. They do not send the private key.

The token header sets `alg` to the signing algorithm. It can also set `kid` to the ID of the
registered key. When `kid` matches a registered key, Code Storage checks the signature with that key
only. Otherwise, Code Storage tries each registered key of the organization.

A decoded token has a header and a set of claims. For example, this token reads and writes
`team/project-alpha`:

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
// Header
{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "V1StGXR8_Z5jdHi6B-myT", // Optional.
}
```

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
// Claims
{
  "iss": "your-org",
  "sub": "ci-pipeline",
  "repo": "team/project-alpha",
  "scopes": ["git:read", "git:write"],
  "iat": 1723453189, // Replace with the current Unix timestamp.
  "exp": 1723456789, // Set an expiration after iat.
}
```

The token claims set its identity, access, and lifetime:

| Claim | Required | Purpose |
| - | - | - |
| `iss` | Yes | Your organization name. |
| `sub` | Yes | A client or task name for logs. |
| `scopes` | Yes | The permissions that the operation needs. See [Scopes](#scopes). |
| `repo` | See [Repository claim](#repository-claim) | The one repository that the token covers. |
| `iat` | For a [restricted key](#restricted-keys) | Issue time as a Unix timestamp in seconds. |
| `exp` | For a [restricted key](#restricted-keys) | Expiration time as a Unix timestamp in seconds. |

Set `exp` on every token. Use the shortest practical lifetime and only the scopes that the operation
needs. A token can also carry [ref policies](/docs/repos/ref-policies) that limit Git writes.

<span id="permission-scopes" />

## Scopes

The `scopes` claim sets the operations that a token can perform. Each scope name must match exactly.
One scope does not include another.

| Scope | Operations |
| - | - |
| `git:read` | Git clone, fetch, and pull. Repository reads: the repository, files, branches, commits, tags, diffs, blame, grep, archive, notes, and merge preview. |
| `git:write` | Git push. Repository writes: branches, tags, commits, merges, notes, and upstream pulls. |
| `repo:write` | Create, update, and delete repositories. Remove a base repository. Create, update, and delete Git credentials. |
| `org:read` | List repositories (`GET /api/repos`) and read service metadata (`GET /api/meta`). |

`git:write` does not include read access. Add `git:read` when a client must clone or fetch before it
pushes. Tag deletion needs both `git:read` and `git:write`.

## Repository claim

The `repo` claim names the one repository that a token covers. The rules depend on the scopes:

* Set `repo` when the token has `git:read`, `git:write`, or `repo:write`. Code Storage rejects such
  a token without `repo`.
* `repo` is optional when the only scope is `org:read`.

When a path contains `{repo_name}`, the path value must equal the `repo` claim exactly. Otherwise
the request returns `404` with the code `repository_not_found`, the same response as for a
repository that does not exist. So one repository-scoped token covers one repository. Percent-encode
a `/` in the path value, for example `team%2Fproject-alpha`.

`POST /api/repos` takes the name of the new repository from the `repo` claim. The optional body
field `repo_name` must equal the claim. Otherwise the request returns `400` with the code
`repo_name_mismatch`.

For example, this token lists the branches of `team/project-alpha` with
`GET /api/repos/team%2Fproject-alpha/branches`:

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "iss": "your-org",
  "sub": "ci-pipeline",
  "repo": "team/project-alpha",
  "scopes": ["git:read"],
  "iat": 1723453189, // Replace with the current Unix timestamp.
  "exp": 1723453489, // Set an expiration after iat.
}
```

This token lists the repositories of the organization with `GET /api/repos`. It has no `repo` claim:

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "iss": "your-org",
  "sub": "repo-indexer",
  "scopes": ["org:read"],
  "iat": 1723453189,
  "exp": 1723453489,
}
```

<span id="restricted-signing-keys" />

## Restricted keys

A restricted key has a scope allowlist, `allowed_scopes`. Its tokens can request only scopes from
that allowlist. A token does not get the allowed scopes automatically: its `scopes` claim must still
list each scope that the operation needs.

Tokens signed with a restricted key must meet these requirements:

* Include both `iat` and `exp` as Unix timestamps in seconds.
* Set `iat` to the issue time.
* Set `exp` after `iat`, at most 3,600 seconds later.
* Include only scopes from the key's allowlist.

For example, a key that allows `git:read` and `org:read` can sign a token with only `git:read`. A
token that also includes `git:write` fails verification, even if the request needs only read access.

An unrestricted key has the allowlist `["*"]` and no one-hour token lifetime limit. Its tokens must
still list valid scopes. `*` is not a valid token scope.

<span id="create-a-jwt-without-an-sdk-client" />

## How to sign a JWT

The [SDK](/docs/sdk/overview) signs and sends JWTs for you.

These examples show how to sign an ES256 token for organization reads without using the SDK. They
use a PKCS8 PEM private key from the dashboard and expire after one hour.

If you use a restricted key, its allowlist must include `org:read`.

The examples read the organization name from `PIERRE_ORG`. For another operation, change the scopes
and add the `repo` claim if the operation needs it.

Install the signing library for your language:

<CodeGroup>
  ```bash TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  pnpm add jose
  ```

  ```bash Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  pip install 'PyJWT[crypto]'
  ```

  ```bash Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  go get github.com/golang-jwt/jwt/v5
  ```
</CodeGroup>

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  import { importPKCS8, SignJWT } from 'jose';

  const key = await importPKCS8(process.env.PIERRE_PRIVATE_KEY!, 'ES256');
  const now = Math.floor(Date.now() / 1000);
  const token = await new SignJWT({
    iss: process.env.PIERRE_ORG!,
    sub: 'api-client',
    scopes: ['org:read'],
    iat: now,
    exp: now + 3600,
  })
    .setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
    .sign(key);

  console.log(token);
  ```

  ```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  import os
  import time

  import jwt

  now = int(time.time())
  token = jwt.encode(
      {
          "iss": os.environ["PIERRE_ORG"],
          "sub": "api-client",
          "scopes": ["org:read"],
          "iat": now,
          "exp": now + 3600,
      },
      os.environ["PIERRE_PRIVATE_KEY"],
      algorithm="ES256",
      headers={"typ": "JWT"},
  )

  print(token)
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  package main

  import (
  	"fmt"
  	"log"
  	"os"
  	"time"

  	"github.com/golang-jwt/jwt/v5"
  )

  func main() {
  	key, err := jwt.ParseECPrivateKeyFromPEM([]byte(os.Getenv("PIERRE_PRIVATE_KEY")))
  	if err != nil {
  		log.Fatal(err)
  	}

  	now := time.Now()
  	token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
  		"iss":    os.Getenv("PIERRE_ORG"),
  		"sub":    "api-client",
  		"scopes": []string{"org:read"},
  		"iat":    now.Unix(),
  		"exp":    now.Add(time.Hour).Unix(),
  	})
  	signed, err := token.SignedString(key)
  	if err != nil {
  		log.Fatal(err)
  	}
  	fmt.Println(signed)
  }
  ```
</CodeGroup>

Each example prints a bearer token. Keep the token out of shared logs.

<span id="base-config" />

Use the token with the [HTTP request setup](/docs/api/overview#base-config) in the API reference.

<span id="resolve-authentication-errors" />

## Resolve token errors

An HTTP error response has a `code` and a `detail`. These responses come from a missing or incorrect
token:

| Status | `code` | Cause |
| - | - | - |
| `400` | `repo_claim_missing` | The operation is repository-scoped and the token has no `repo` claim. |
| `400` | `repo_name_mismatch` | The `repo_name` body field of `POST /api/repos` does not equal the `repo` claim. |
| `401` | `unauthorized` | The request has no `Authorization` header, or the header uses an unsupported scheme. |
| `403` | `forbidden` | With the detail `Invalid or expired token`, Code Storage cannot verify the token. See the checks below. |
| `403` | `forbidden` | With the detail `Credential is not valid for this tenant`, the token is for a different organization than the host. |
| `403` | `authentication_unavailable` | Code Storage cannot verify tokens now. Retry the request. |
| `403` | `insufficient_scope` | The token does not have a scope that the operation needs. |
| `404` | `repository_not_found` | The `{repo_name}` path value does not equal the `repo` claim, or the repository does not exist. |

For `Invalid or expired token`, check these items:

* The signature uses a registered key, and `alg` is ES256, ES384, ES512, or RS256.
* `iss` is your organization name.
* `exp` is in the future.
* `iss`, `sub`, and `scopes` are present, and each scope name is in [Scopes](#scopes).
* `repo` is present when a scope needs it. See [Repository claim](#repository-claim).
* A token from a restricted key meets the [restricted key requirements](#restricted-keys).

For all other HTTP error codes, see [Error codes](/docs/api/overview#error-codes) in the API reference.

## Rotate or revoke a key

To rotate a key, create a replacement, update your token signers, then delete the old key from the
dashboard. When you delete a key, Code Storage rejects the tokens that its private key signed.
