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

# Proxy Git with Your Own Auth

> Give your users Git remotes on your own domain. Authenticate each request with your own system, then forward it to Code Storage with a short-lived token.

Use this pattern when you give your users direct Git access. Your users clone and push with a remote
on your domain, such as `https://git.your-app.com/team/project-alpha.git`. Your server authenticates
each request with your own system and forwards it to Code Storage with a short-lived token.

Your users never receive a Code Storage token or your private key. You control:

* **Authentication.** Use your own sign-in, access tokens, or SSO.
* **Authorization.** Decide which user can read or write each repository, and set
  [ref policies](/docs/repos/ref-policies) for each user.
* **Revocation.** When you remove access in your system, the next request fails.
* **Access logs.** Record the user, IP address, repository, and operation of each request.

## Choose an approach

A proxy gives you the most control, but it is also the most work: you run a server in the path of
every clone and push. Two lighter options do not need a proxy:

* **Short-lived remote URLs.** Your backend gives each user a remote URL with a short-lived token.
  See [`repo.getRemoteURL()`](/docs/sdk/repos/remote-url-get).
* **A Git credential helper.** You ship a
  [credential helper](https://git-scm.com/docs/gitcredentials) that gets a short-lived token from
  your API each time Git needs credentials.

With these options, your users hold a Code Storage token until it expires, and their requests go
directly to Code Storage. Use a proxy when your users must never hold a token, or when you must
check, revoke, and log each request.

## How it works

```text theme={"theme":{"light":"github-light","dark":"min-dark"}}
git ──▶ your proxy ──▶ your-org.code.storage
        1. Authenticate the user.
        2. Check access to the repository.
        3. Sign a token for this request.
        4. Forward the request and stream the response back.
```

Your proxy must:

* Forward only the Git endpoints below, and return `404` for every other path.
* Return `401` when the user's credentials are missing or incorrect, and `404` when the user cannot
  access the repository.
* Sign a new token for each request, with the repository, the scopes that the request needs, your
  user ID as `sub`, and your ref policies.
* Never forward the user's `Authorization` header to Code Storage.
* Stream the request body and the response body.

## The endpoints that Git uses

Git over HTTPS uses four requests for each repository, under `/{repo}.git/`:

| Request | Git command | Scopes |
| - | - | - |
| `GET info/refs?service=git-upload-pack` | `clone`, `fetch` | `git:read` |
| `POST git-upload-pack` | `clone`, `fetch` | `git:read` |
| `GET info/refs?service=git-receive-pack` | `push` | `git:read`, `git:write` |
| `POST git-receive-pack` | `push` | `git:read`, `git:write` |

<Note>
  The example proxy does not support [Git LFS](/docs/repos/git-lfs) yet. It returns `404` for LFS
  requests.
</Note>

## Build the proxy

The proxy is a [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) handler: it
takes a `Request` and returns a `Response`. This example runs it on Node.js with
[srvx](https://srvx.h3.dev) and signs tokens with [`jose`](https://github.com/panva/jose). Install
the packages:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
pnpm add jose srvx
```

Set `PIERRE_ORG` and `PIERRE_PRIVATE_KEY` as shown in [Authentication](/docs/platform/authentication).

Each step below adds code to `proxy.ts`, in order. The [complete file](#the-complete-proxy) is at
the end of this section.

### Set up

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { importPKCS8, SignJWT } from 'jose';
import { serve } from 'srvx';

// Your own auth logic. See "Connect your user system".
import { authenticate, authorize } from './auth.ts';

const org = process.env.PIERRE_ORG!;
const codeStorage = `https://${org}.code.storage`;
const keyPem = process.env.PIERRE_PRIVATE_KEY!;
const signingKey = await importPKCS8(keyPem, 'ES256');

type Access = 'read' | 'write';
```

### Match the Git endpoints

Each route sets the access that its endpoint needs. A request that matches no route gets `404`.

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// The four Git endpoints, and the access that each one needs.
const routes = new Map<string, Access>([
  ['GET info/refs?service=git-upload-pack', 'read'],
  ['POST git-upload-pack', 'read'],
  ['GET info/refs?service=git-receive-pack', 'write'],
  ['POST git-receive-pack', 'write'],
]);

type Route = { repoId: string; path: string; access: Access };

// Match /{repo}.git/{path}. A repository ID can contain slashes.
function matchRoute(req: Request): Route | null {
  const url = new URL(req.url);
  const match = url.pathname.match(/^\/(.+?)\.git\/(.+)$/);
  if (!match) return null;
  const [, repoId, endpoint] = match;
  const path = endpoint + url.search;
  const access = routes.get(`${req.method} ${path}`);
  return access ? { repoId, path, access } : null;
}
```

### Check the user

The handler authenticates the user, checks access to the repository, and writes one log line for
each request. The line has the user, IP address, repository, endpoint, and response status.

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// Handle one Git request. `ip` is the client IP, for the log.
async function handleGit(req: Request, ip: string) {
  const route = matchRoute(req);
  if (!route) return notFound();

  const user = await authenticate(req);
  if (!user) {
    return new Response('Authentication required\n', {
      status: 401,
      headers: { 'WWW-Authenticate': 'Basic realm="git"' },
    });
  }
  // Return 404, so that a user cannot find repositories that
  // they cannot access.
  const { repoId, path, access } = route;
  if (!(await authorize(user, repoId, access))) return notFound();

  const token = await signToken(user.id, repoId, access);
  const url = `${codeStorage}/${repoId}.git/${path}`;
  const res = await forward(req, url, token);

  const entry = { user: user.id, ip, repoId, path };
  console.log(JSON.stringify({ ...entry, status: res.status }));
  return res;
}

function notFound() {
  return new Response('Not found\n', { status: 404 });
}
```

### Connect your user system

Put your own logic in `auth.ts`. It exports two functions:

* **`authenticate(request)`** returns the user, as an object with an `id`, or `null`. Git sends the
  user's credentials as HTTP Basic auth in the `Authorization` header. For `null`, the proxy returns
  `401`, and Git asks the user or its credential helper for credentials.
* **`authorize(user, repo, access)`** returns `true` if the user can access the repository. `access`
  is `'read'` for a clone or fetch, and `'write'` for a push. For `false`, the proxy returns `404`,
  so a user cannot find out which repositories exist.

### Sign a token

The token covers one repository and the scopes that the request needs. It sets `sub` to your user
ID, which Code Storage shows in its logs. The token expires after 300 seconds. It only needs to be
valid when Code Storage receives the request, so a long push still completes.

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// Sign a short-lived Code Storage token for one request.
function signToken(sub: string, repo: string, access: Access) {
  const scopes = ['git:read'];
  if (access === 'write') scopes.push('git:write');

  const now = Math.floor(Date.now() / 1000);
  return new SignJWT({
    iss: org,
    sub, // Your user ID. Code Storage shows it in logs.
    repo,
    scopes,
    // Ref policies for this user.
    refs: [['refs/heads/main', ['no-force-push']]],
    iat: now,
    exp: now + 300,
  })
    .setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
    .sign(signingKey);
}
```

<Note>
  If you use a [restricted key](/docs/platform/authentication#restricted-keys), its allowlist must
  include `git:read` and `git:write`. A restricted key also limits the token lifetime to 3,600
  seconds.
</Note>

### Forward the request

The proxy forwards only the headers that Git needs. It does not forward the user's `Authorization`
header. A clone or push can transfer a large pack, so the proxy streams both bodies and does not
buffer them.

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// The request headers that Git needs.
const requestHeaders = [
  'accept',
  'content-type',
  'content-encoding', // Compressed requests.
  'git-protocol', // Git protocol v2.
  'user-agent',
];
const responseHeaders = ['content-type', 'cache-control'];

// Forward the request with the token. Stream both bodies.
async function forward(req: Request, url: string, token: string) {
  const auth = `Basic ${btoa(`t:${token}`)}`;
  const headers = new Headers({ Authorization: auth });
  for (const name of requestHeaders) {
    const value = req.headers.get(name);
    if (value) headers.set(name, value);
  }

  const res = await fetch(url, {
    method: req.method,
    headers,
    body: req.body,
    duplex: 'half',
  } as RequestInit);

  const resHeaders = new Headers();
  for (const name of responseHeaders) {
    const value = res.headers.get(name);
    if (value) resHeaders.set(name, value);
  }
  const init = { status: res.status, headers: resHeaders };
  return new Response(res.body, init);
}
```

### Serve the proxy

```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
serve({
  port: 8080,
  fetch: (req) => handleGit(req, req.ip ?? 'unknown'),
});
```

`handleGit()` uses only the Fetch API. To run it on another server, pass it each `Request` and the
client IP address, and return its `Response`.

Run the proxy with Node.js 24 or later. Node.js runs TypeScript files directly. Set
`"type": "module"` in `package.json`.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
node proxy.ts
```

### The complete proxy

<Accordion title="proxy.ts">
  ```typescript proxy.ts theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  import { importPKCS8, SignJWT } from 'jose';
  import { serve } from 'srvx';

  // Your own auth logic. See "Connect your user system".
  import { authenticate, authorize } from './auth.ts';

  const org = process.env.PIERRE_ORG!;
  const codeStorage = `https://${org}.code.storage`;
  const keyPem = process.env.PIERRE_PRIVATE_KEY!;
  const signingKey = await importPKCS8(keyPem, 'ES256');

  type Access = 'read' | 'write';

  // The four Git endpoints, and the access that each one needs.
  const routes = new Map<string, Access>([
    ['GET info/refs?service=git-upload-pack', 'read'],
    ['POST git-upload-pack', 'read'],
    ['GET info/refs?service=git-receive-pack', 'write'],
    ['POST git-receive-pack', 'write'],
  ]);

  type Route = { repoId: string; path: string; access: Access };

  // Match /{repo}.git/{path}. A repository ID can contain slashes.
  function matchRoute(req: Request): Route | null {
    const url = new URL(req.url);
    const match = url.pathname.match(/^\/(.+?)\.git\/(.+)$/);
    if (!match) return null;
    const [, repoId, endpoint] = match;
    const path = endpoint + url.search;
    const access = routes.get(`${req.method} ${path}`);
    return access ? { repoId, path, access } : null;
  }

  // Handle one Git request. `ip` is the client IP, for the log.
  async function handleGit(req: Request, ip: string) {
    const route = matchRoute(req);
    if (!route) return notFound();

    const user = await authenticate(req);
    if (!user) {
      return new Response('Authentication required\n', {
        status: 401,
        headers: { 'WWW-Authenticate': 'Basic realm="git"' },
      });
    }
    // Return 404, so that a user cannot find repositories that
    // they cannot access.
    const { repoId, path, access } = route;
    if (!(await authorize(user, repoId, access))) return notFound();

    const token = await signToken(user.id, repoId, access);
    const url = `${codeStorage}/${repoId}.git/${path}`;
    const res = await forward(req, url, token);

    const entry = { user: user.id, ip, repoId, path };
    console.log(JSON.stringify({ ...entry, status: res.status }));
    return res;
  }

  function notFound() {
    return new Response('Not found\n', { status: 404 });
  }

  // Sign a short-lived Code Storage token for one request.
  function signToken(sub: string, repo: string, access: Access) {
    const scopes = ['git:read'];
    if (access === 'write') scopes.push('git:write');

    const now = Math.floor(Date.now() / 1000);
    return new SignJWT({
      iss: org,
      sub, // Your user ID. Code Storage shows it in logs.
      repo,
      scopes,
      // Ref policies for this user.
      refs: [['refs/heads/main', ['no-force-push']]],
      iat: now,
      exp: now + 300,
    })
      .setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
      .sign(signingKey);
  }

  // The request headers that Git needs.
  const requestHeaders = [
    'accept',
    'content-type',
    'content-encoding', // Compressed requests.
    'git-protocol', // Git protocol v2.
    'user-agent',
  ];
  const responseHeaders = ['content-type', 'cache-control'];

  // Forward the request with the token. Stream both bodies.
  async function forward(req: Request, url: string, token: string) {
    const auth = `Basic ${btoa(`t:${token}`)}`;
    const headers = new Headers({ Authorization: auth });
    for (const name of requestHeaders) {
      const value = req.headers.get(name);
      if (value) headers.set(name, value);
    }

    const res = await fetch(url, {
      method: req.method,
      headers,
      body: req.body,
      duplex: 'half',
    } as RequestInit);

    const resHeaders = new Headers();
    for (const name of responseHeaders) {
      const value = res.headers.get(name);
      if (value) resHeaders.set(name, value);
    }
    const init = { status: res.status, headers: resHeaders };
    return new Response(res.body, init);
  }

  serve({
    port: 8080,
    fetch: (req) => handleGit(req, req.ip ?? 'unknown'),
  });
  ```
</Accordion>

## Use the remote

Give your users the URL of your proxy. Git sends their credentials with HTTP Basic auth, so they can
use a Git credential helper as they do with any other host:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
git clone https://git.your-app.com/team/project-alpha.git
```

When Git asks for credentials, the user enters the credentials that your `authenticate()` accepts,
for example a username and an access token from your product. The proxy matches repository paths
that end in `.git`, so give your users URLs in this form.

## Attribute pushes

[Push webhooks](/docs/repos/webhooks) identify the repository and ref, but not the user who pushed. Your
proxy log identifies the user for each `git-receive-pack` request. Use your proxy log as the record
of who pushed.

## Test the proxy

Run these checks before you give the remote to users:

1. Clone a repository through the proxy with a valid user.
2. Push to a branch that the user can write.
3. Clone with an incorrect token. The proxy returns `401`.
4. Clone a repository that the user cannot access. The proxy returns `404`.
5. Push to a ref that a ref policy blocks. Git shows `push denied by policy`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.