> ## 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 & Security

> Create an API key, store it, sign JWTs, and set permission scopes and ref policies for Code Storage.

Authentication uses ES256, ES384, ES512, or RS256 JWT tokens signed with your private key.

## Create an API key

Create an API key from the **API Keys** page in your organization dashboard.

Your browser creates an ECDSA P-256 key pair and registers the public key with Code Storage. Copy
the private key and your organization identifier.

<Warning>
  Copy the private key now. Code Storage cannot show it again. The key grants access to your Code
  Storage organization. Do not commit it.
</Warning>

Code Storage stores only the public key. It uses the `iss` claim to select that key when it checks a
JWT.

Delete an API key from the same page to revoke it. Code Storage then rejects every JWT that the key
signed.

<Note>
  **API Keys** sign JWTs for API and Git requests. Code Storage uses [**Signing
  Keys**](/docs/guides/commit-signing) to check commit signatures.
</Note>

## 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.

The SDK examples read the key from a `PIERRE_PRIVATE_KEY` environment variable.

## JWTs

The SDK creates JWTs with your private API key and includes the JWT in each Git remote URL.

Each JWT uses these claims to set its identity, access, and lifetime:

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "iss": "your-org", // Set your organization identifier.
  "sub": "ci-pipeline-prod", // Set the client identifier for logs.
  "repo": "team/project-alpha", // Limit the JWT to this repository. Omit it for org:read.
  "scopes": ["git:read", "git:write"], // Set the exact permissions. See Permission scopes below.
  "iat": 1723453189, // Set the issue time as a Unix timestamp.
  "exp": 1723456789, // Set the expiration time as a Unix timestamp.
}
```

Set this JWT header:

```jsonc theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "alg": "ES256", // Use ES256, ES384, ES512, or RS256.
  "typ": "JWT", // Set the type to JWT.
}
```

Use the shortest practical lifetime for each JWT. Give each JWT only the scopes that its client
needs. Use a unique `sub` value for each client or task. Add a ref policy when a JWT must write only
selected refs. Keep the private API key on a server or in a secret manager.

## Permission scopes

| Scope        | Description                             | Operations                                                 |
| ------------ | --------------------------------------- | ---------------------------------------------------------- |
| `git:read`   | Read repository contents.               | clone, fetch, pull                                         |
| `git:write`  | Modify repository contents.             | push                                                       |
| `repo:write` | Create, update, or delete repositories. | [POST /api/repos](/docs/reference/api/repositories/create-repo) |
| `org:read`   | List repositories in an organization.   | [GET /api/repos](/docs/reference/api/repositories/list-repos)   |

Code Storage matches each scope exactly. One scope does not include another scope.

`git:write` does not grant read access. Add `git:read` when a client must clone or fetch before it
pushes.

## Limit ref writes

The `git:write` scope permits updates to all refs by default. Add a ref policy when a JWT needs
narrower write access.

A ref policy can limit branches, tags, notes, and refs in a namespace. It can also reject force
pushes or require commit signatures.

See [Ref Policies](/docs/guides/ref-policies) for the JWT claim and SDK options. See
[Commit Signing](/docs/guides/commit-signing) for key setup and signature checks.

## Create a JWT without an SDK client

Create a JWT directly when a custom Git tool or service cannot use an SDK client. The examples
accept a PKCS8 PEM private key.

The TypeScript and Go examples use `ES256`. The Python helper selects `ES256` or `RS256` from the
key type.

<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: 'your-org',
    sub: 'ci-pipeline-prod',
    repo: 'team/project-alpha',
    scopes: ['git:read', 'git:write'],
    iat: now,
    exp: now + 3600,
  })
    .setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
    .sign(key);

  const gitURL = `https://t:${token}@your-org.code.storage/team/project-alpha.git`;
  console.log(`git clone ${gitURL}`);
  ```

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

  from pierre_storage import generate_jwt

  private_key = os.environ["PIERRE_PRIVATE_KEY"]

  token = generate_jwt(
      key_pem=private_key,  # Required. This key signs the JWT.
      issuer="your-org",  # Required. This parameter maps to iss.
      repo_id="team/project-alpha",  # Required. This parameter maps to repo.
      scopes=["git:read", "git:write"],  # Optional. This list maps to scopes and is the default.
      ttl=3600,  # Optional. This value sets exp from iat. The default is 31536000 seconds.
  )

  git_url = f"https://t:{token}@your-org.code.storage/team/project-alpha.git"
  print(f"git clone {git_url}")
  ```

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

  import (
  	"crypto/ecdsa"
  	"crypto/x509"
  	"encoding/pem"
  	"fmt"
  	"log"
  	"os"
  	"time"

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

  func main() {
  	keyPEM := []byte(os.Getenv("PIERRE_PRIVATE_KEY"))

  	privateKey, err := parseECPrivateKey(keyPEM)
  	if err != nil {
  		log.Fatalf("parse private key: %v", err)
  	}

  	now := time.Now()
  	claims := jwt.MapClaims{
  		"iss":    "your-org",
  		"sub":    "ci-pipeline-prod",
  		"repo":   "team/project-alpha",
  		"scopes": []string{"git:read", "git:write"},
  		"iat":    now.Unix(),
  		"exp":    now.Add(time.Hour).Unix(),
  	}

  	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
  	signed, err := token.SignedString(privateKey)
  	if err != nil {
  		log.Fatalf("sign token: %v", err)
  	}

  	cloneURL := fmt.Sprintf("https://t:%s@your-org.code.storage/team/project-alpha.git", signed)
  	fmt.Println("git clone", cloneURL)
  }

  func parseECPrivateKey(pemBytes []byte) (*ecdsa.PrivateKey, error) {
  	block, _ := pem.Decode(pemBytes)
  	if block == nil {
  		return nil, fmt.Errorf("decode private key PEM")
  	}

  	if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
  		if ecKey, ok := key.(*ecdsa.PrivateKey); ok {
  			return ecKey, nil
  		}
  		return nil, fmt.Errorf("private key is not ECDSA")
  	}

  	if ecKey, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
  		return ecKey, nil
  	}

  	return nil, fmt.Errorf("unsupported private key format")
  }
  ```
</CodeGroup>
