mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
66c89db942
Signed-off-by: jeffhuang <jeffwalt630@gmail.com> Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
117 lines
4.0 KiB
Markdown
117 lines
4.0 KiB
Markdown
# OIDC Proxy
|
|
|
|
A Cloudflare Worker that authenticates GitHub Actions OIDC tokens and proxies requests to an upstream API with an injected API key. This lets CI workflows call APIs without storing long-lived secrets in GitHub.
|
|
|
|
## How it works
|
|
|
|
```
|
|
GitHub Actions (OIDC token) → Worker (validate JWT, inject API key) → Upstream API
|
|
```
|
|
|
|
1. A GitHub Actions workflow mints an OIDC token with a configured audience
|
|
2. The workflow sends requests to this proxy, passing the OIDC token as the API key
|
|
3. The worker validates the JWT against GitHub's JWKS, checks issuer/audience/age/repo
|
|
4. If valid, the request is forwarded to the upstream API with the real API key injected
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
cd oidc-proxy
|
|
npm install
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Edit `wrangler.toml` for your upstream:
|
|
|
|
| Variable | Description |
|
|
|---|---|
|
|
| `OIDC_ISSUER` | `https://token.actions.githubusercontent.com` |
|
|
| `OIDC_AUDIENCE` | The audience your workflow requests (e.g. `goose-oidc-proxy`) |
|
|
| `MAX_TOKEN_AGE_SECONDS` | Operator-configured upper bound on `iat` age in seconds (default: `1200` = 20 min). Applied **in addition to** the IdP's `exp` claim, never as a replacement. |
|
|
| `MAX_REQUESTS_PER_TOKEN` | Max requests per OIDC token (default: `200`) |
|
|
| `RATE_LIMIT_PER_SECOND` | Max requests per second per token (default: `2`) |
|
|
| `ALLOWED_REPOS` | *(optional)* Comma-separated `owner/repo` list |
|
|
| `ALLOWED_REFS` | *(optional)* Comma-separated allowed refs |
|
|
| `UPSTREAM_URL` | The upstream API base URL |
|
|
| `UPSTREAM_AUTH_HEADER` | Header name for the API key (e.g. `x-api-key`, `Authorization`) |
|
|
| `UPSTREAM_AUTH_PREFIX` | *(optional)* Prefix before the key (e.g. `Bearer `) — omit for raw value |
|
|
| `CORS_ORIGIN` | *(optional)* Allowed CORS origin |
|
|
| `CORS_EXTRA_HEADERS` | *(optional)* Additional CORS allowed headers |
|
|
|
|
Set your upstream API key as a secret:
|
|
|
|
```bash
|
|
npx wrangler secret put UPSTREAM_API_KEY
|
|
```
|
|
|
|
### Example: Anthropic
|
|
|
|
```toml
|
|
UPSTREAM_URL = "https://api.anthropic.com"
|
|
UPSTREAM_AUTH_HEADER = "x-api-key"
|
|
CORS_EXTRA_HEADERS = "anthropic-version"
|
|
```
|
|
|
|
### Example: OpenAI-compatible
|
|
|
|
```toml
|
|
UPSTREAM_URL = "https://api.openai.com"
|
|
UPSTREAM_AUTH_HEADER = "Authorization"
|
|
UPSTREAM_AUTH_PREFIX = "Bearer "
|
|
```
|
|
|
|
## Usage in GitHub Actions
|
|
|
|
```yaml
|
|
permissions:
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Get OIDC token
|
|
id: oidc
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const token = await core.getIDToken('goose-oidc-proxy');
|
|
core.setOutput('token', token);
|
|
core.setSecret(token);
|
|
|
|
- name: Call API through proxy
|
|
env:
|
|
ANTHROPIC_BASE_URL: https://oidc-proxy.your-subdomain.workers.dev
|
|
ANTHROPIC_API_KEY: ${{ steps.oidc.outputs.token }}
|
|
run: goose run --recipe my-recipe.yaml
|
|
```
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
## Deploy
|
|
|
|
```bash
|
|
npx wrangler secret put UPSTREAM_API_KEY
|
|
npm run deploy
|
|
```
|
|
|
|
## Token budget and rate limiting
|
|
|
|
Each OIDC token is tracked by its `jti` (JWT ID) claim using a Durable Object. This provides:
|
|
|
|
- **Budget**: Each token is limited to `MAX_REQUESTS_PER_TOKEN` total requests (default: 200). Once exhausted, the proxy returns `429` with `{"error": "Token budget exhausted"}`.
|
|
- **Rate limit**: Each token is limited to `RATE_LIMIT_PER_SECOND` requests per second (default: 2). When exceeded, the proxy returns `429` with `{"error": "Rate limit exceeded"}` and a `Retry-After: 1` header.
|
|
|
|
Both limits are enforced atomically — the Durable Object processes one request at a time per token, so there are no race conditions.
|
|
|
|
## Token age vs expiry
|
|
|
|
The proxy enforces **both** gates and a token must pass each:
|
|
|
|
1. The IdP's `exp` claim (always enforced).
|
|
2. The operator's `MAX_TOKEN_AGE_SECONDS` cap on `iat`, when configured (default `1200`s = 20 min).
|
|
|
|
`MAX_TOKEN_AGE_SECONDS` is a stricter upper bound *on top of* `exp` — it cannot extend a token past its `exp`. For workflows longer than the IdP's token lifetime (GitHub OIDC issues `exp = iat + 300` ≈ 5 min), refresh the OIDC token rather than relying on `MAX_TOKEN_AGE_SECONDS` to accept expired tokens.
|