> ## Documentation Index
> Fetch the complete documentation index at: https://proxy-hopper.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Server

> Deploying, configuring, and operating the external token server that backs Proxy Hopper's managed auth feature.

## Overview

When a target has upstream credentials that expire and need refreshing — OAuth access tokens, session cookies, rotating API keys — Proxy Hopper can offload that lifecycle to a **token server**: a small HTTP service someone on your team writes and deploys, which Proxy Hopper calls before forwarding requests to `authManaged: true` targets.

Proxy Hopper doesn't ship a token server itself — it's application-specific, since only you know how to authenticate against the upstream API. This page covers deploying and operating one; see [Building a Token Server](/developers/token-server) for implementing one, and [Managed Auth](/concepts/managed-auth) for the underlying concept.

<Note>
  The token server is a separate deployable — its own container image, its own process, scaled independently of Proxy Hopper. Proxy Hopper only needs its URL.
</Note>

## Before deploying: verify credentials are scoped per IP

This is worth checking before you put a token server into production, whether you wrote it or inherited it from another team.

Proxy Hopper exists to spread requests across a rotating IP pool so a target can't easily attribute them all to one caller. It calls the token server separately for every `(target, proxy-IP)` pair specifically so each IP *can* carry its own distinct credential — but nothing stops a token server from ignoring the IP it's given and handing back the same static token for every request regardless. Proxy Hopper will still cache that per IP without complaint; the failure mode is silent. If it happens, the token becomes the one constant tying every proxy IP back to the same caller, which quietly defeats the reason the pool exists.

This is only a real problem for auth mechanisms that support per-session or per-login credentials (most OAuth authorization flows, session cookies) — a token server backing a single organisation-wide API key with no session concept is correctly returning one shared credential, because there's nothing to scope.

To check: call `POST /token` on the token server directly with two different fake `ip` values for the same `target` and compare the responses.

```bash theme={}
curl -s -X POST http://token-server:9000/token -d '{"target":"my-api","ip":"1.1.1.1","port":3128,"cursor":{}}' | python -m json.tool
curl -s -X POST http://token-server:9000/token -d '{"target":"my-api","ip":"2.2.2.2","port":3128,"cursor":{}}' | python -m json.tool
```

If the target's auth is session/login-based and both calls return the same `Authorization` (or `Cookie`) value, that's a bug in the token server, not expected behavior — see [Building a Token Server — scope credentials per IP](/developers/token-server#scope-credentials-per-ip) for what the implementation should be doing instead.

## Enabling managed auth

**1. Point Proxy Hopper at the token server** via `server.authServer`:

```yaml theme={}
server:
  authServer:
    url: "http://token-server:9000"
    timeoutSeconds: 10
    refreshThresholdSeconds: 60
    retryIntervalSeconds: 30
    maxRetries: 5
    exposeProxyUrl: false
```

**2. Flag the targets that need it:**

```yaml theme={}
targets:
  - name: my-api
    regex: 'api\.example\.com'
    ipPool: my-pool
    authManaged: true
```

### `server.authServer` fields

| Field                     | Default  | Description                                                                                                                                                                                                                      |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`                     | required | Base URL of the token server, e.g. `http://token-server:9000`                                                                                                                                                                    |
| `timeoutSeconds`          | `10`     | Hard timeout for each `POST /token` call                                                                                                                                                                                         |
| `refreshThresholdSeconds` | `60`     | Proactively refresh a token this many seconds before `expires_at`                                                                                                                                                                |
| `retryIntervalSeconds`    | `30`     | Cooldown before retrying a failed token server call, and before attempting recovery from `AUTH_BROKEN`                                                                                                                           |
| `maxRetries`              | `5`      | Consecutive token server failures before an IP is marked `AUTH_BROKEN`                                                                                                                                                           |
| `exposeProxyUrl`          | `true`   | Include Proxy Hopper's own public URL in the `POST /token` request body, so the token server can route its own auth calls back through a pinned IP — see [IP-pinned acquisition](/developers/token-server#ip-pinned-acquisition) |

<Warning>
  `server.authServer` is only configurable via the YAML config file — there are no `PROXY_HOPPER_AUTH_SERVER_*` environment variables or CLI flags for it. See [Config Reference](/admin/configuration/reference#authserver) for how this fits alongside the rest of `server:`.
</Warning>

There is one required field per target:

| Field         | Default | Description                                                                                                                     |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `authManaged` | `false` | When `true`, Proxy Hopper calls the token server before forwarding each request to this target and injects the returned headers |

## Broken-state and recovery

Token server failures are tracked per proxy IP, not globally — one IP having trouble doesn't stop the others from working:

1. A failed `POST /token` call is retried after `retryIntervalSeconds`.
2. After `maxRetries` consecutive failures for that IP, it's marked `AUTH_BROKEN` — subsequent requests through that IP fail fast with `502`, without calling the token server again.
3. After a further `retryIntervalSeconds`, Proxy Hopper attempts recovery. Success returns the IP to normal rotation; failure restarts the cooldown.

The IP itself is **not** quarantined — this is independent of the regular quarantine mechanism (see [IP Pools](/concepts/ip-pools)). A target can have IPs simultaneously in normal rotation, quarantined for connection failures, and `AUTH_BROKEN` for auth failures.

<Tip>
  If every IP for a target enters `AUTH_BROKEN` at once, that's almost always the token server itself being down or misconfigured, not a per-IP problem — check its `/health` endpoint and logs first.
</Tip>

## Observability

### Metrics

With `server.metrics: true`, four auth-specific Prometheus metrics are exposed alongside the standard ones:

| Metric                                              | Type      | Labels                   | Description                                                 |
| --------------------------------------------------- | --------- | ------------------------ | ----------------------------------------------------------- |
| `proxy_hopper_auth_token_refreshes_total`           | Counter   | `target`, `ip`, `status` | Token refresh attempts — `status` is `success` or `failure` |
| `proxy_hopper_auth_token_refresh_duration_seconds`  | Histogram | `target`, `ip`           | End-to-end refresh duration                                 |
| `proxy_hopper_auth_broken_ips_current`              | Gauge     | `target`                 | IPs currently in `AUTH_BROKEN` state                        |
| `proxy_hopper_auth_server_request_duration_seconds` | Histogram | —                        | Raw HTTP round-trip to the token server                     |

See [Prometheus Metrics](/admin/observability/metrics#managed-auth-metrics) for the full reference alongside the rest of Proxy Hopper's metrics.

<Tip>
  `proxy_hopper_auth_broken_ips_current` is the single most useful alert to wire up for managed auth — a sustained non-zero value means the token server is failing for real traffic, not just a transient blip.
</Tip>

### Structured logs

At `logFormat: json`, these `event` values appear in log lines related to managed auth:

| `event`                    | Level   | Meaning                                                                                                                       |
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `token_prewarm_started`    | INFO    | Startup pre-warm begun — Proxy Hopper calls the token server for all configured `(target, ip)` pairs before accepting traffic |
| `token_acquired`           | INFO    | Token fetched or refreshed successfully                                                                                       |
| `token_refresh_triggered`  | DEBUG   | Near-expiry refresh initiated                                                                                                 |
| `token_server_timeout`     | WARNING | Token server did not respond within `timeoutSeconds`                                                                          |
| `token_server_error`       | WARNING | Token server returned non-200 or the connection failed                                                                        |
| `ip_marked_auth_broken`    | ERROR   | IP reached `maxRetries` consecutive failures                                                                                  |
| `auth_recovery_attempt`    | INFO    | Retry cooldown elapsed; the next request will retry the token server                                                          |
| `ip_recovered_auth_broken` | INFO    | Recovery succeeded; IP is back in normal rotation                                                                             |

## Startup pre-warming

At startup, Proxy Hopper calls `GET /health` on the token server and waits for a `2xx` before proceeding, then proactively calls `POST /token` for every `(target, proxy-IP)` pair across all `authManaged` targets. This means the first real client request never pays the cost of an on-demand token fetch — by the time Proxy Hopper accepts traffic, every IP already has a cached, valid token.

This also means a token server that's slow to start, or slow per call, directly delays Proxy Hopper's own startup for large IP pools — size `timeoutSeconds` and the token server's own capacity accordingly.

## Deploying a token server

How you build the image is entirely up to you — see [Building a Token Server](/developers/token-server) for the two supported approaches (the `proxy-hopper-token-server` library, or a hand-rolled service implementing the same two-endpoint contract). For running it once built:

<CardGroup cols={2}>
  <Card title="Helm" icon="dharmachakra" href="/admin/deployment/kubernetes/helm#token-server">
    Set `tokenServer.enabled: true` and the chart deploys a Deployment + Service for your image, and derives the in-cluster URL for `server.authServer.url` automatically.
  </Card>

  <Card title="Docker Compose" icon="docker">
    Run the token server as its own service alongside Proxy Hopper — see the [Docker Compose example](https://github.com/cams-data/proxy-hopper-v2/tree/next/examples/docker-compose/token-server) in the repo.
  </Card>

  <Card title="Kubernetes manifests" icon="dharmachakra">
    Raw manifests are available in the [Kubernetes example](https://github.com/cams-data/proxy-hopper-v2/tree/next/examples/kubernetes) for non-Helm deployments.
  </Card>

  <Card title="Standalone" icon="server">
    Any container platform works — the token server is a plain HTTP service with no dependency on Proxy Hopper's process or backend. Point `server.authServer.url` at wherever it lives.
  </Card>
</CardGroup>

### Scaling and availability

The token server should be able to run multiple replicas behind its own load balancer/Service if you need HA — nothing about the contract assumes a single instance. If your provider tokens require coordination across replicas (e.g. a shared refresh-token store), that coordination is your token server's responsibility; Proxy Hopper's own cursor mechanism only guarantees the *same* cursor is passed back for a given `(target, proxy-IP)` pair, regardless of which token server replica handled the previous call.

<Warning>
  If you run Proxy Hopper itself with multiple replicas and the `memory` backend, each replica has its own independent token cache — the same `(target, IP)` pair may be re-acquired separately by each replica. Use the `redis` backend to share the token cache (and the rest of pool state) across replicas. See [High Availability](/admin/deployment/kubernetes/high-availability).
</Warning>
