# EvalGate authentication for people and agents

EvalGate uses interactive GitHub or Google sign-in for people and scoped
EvalGate API keys for agents, SDKs, CI, REST, A2A, and MCP calls. The human CLI
uses a distinct revocable `cli_session`, not an implicitly minted repository
key.
For clients that implement OAuth, EvalGate exposes authorization-code with PKCE
and a server-owned RFC 8628 device grant. Both preserve the human approval and
organization boundary. The `agent_auth` profile additionally supports
`service_auth`: an agent names its intended human approver, who must sign in
with that verified email and approve the organization and scopes. Third-party
identity assertions and anonymous hosted registrations are not accepted.

## Discover

Start with the RFC 9728 Protected Resource Metadata document:

- `https://www.evalgate.com/.well-known/oauth-protected-resource` (origin)
- `https://www.evalgate.com/.well-known/oauth-protected-resource/api` (API)
- `https://www.evalgate.com/.well-known/oauth-authorization-server`
- API description: `https://www.evalgate.com/openapi.json`
- This walkthrough: `https://www.evalgate.com/auth.md`

Protected API entry points return a `401` Bearer challenge whose
`resource_metadata` parameter points back to the protected-resource document.
For example:

```http
WWW-Authenticate: Bearer resource_metadata="https://www.evalgate.com/.well-known/oauth-protected-resource/api/mcp"
```

The protected-resource metadata links to the RFC 8414 authorization-server
document. That document advertises authorization-code with PKCE, dynamic public
client registration, the RFC 8628 device grant, and the scopes a person can
approve. EvalGate's documentation assigns the shared
public `client_id` `evalgate-agent`; RFC 8414 has no standard field for listing
individual public client IDs. EvalGate does not issue refresh tokens or support
third-party ID-JAG exchange.

## Agentic registration (`service_auth`)

The authorization-server metadata's `agent_auth` block links to `/oauth/identity`,
`/oauth/claim`, and this guide. Its `identity_types_supported` is
`["service_auth"]`. An email is a routing hint, never proof of identity. Only a
signed-in organization admin with that verified email can complete the existing
consent screen. Starting registration creates no account, organization, or key.

```bash
curl -X POST https://www.evalgate.com/oauth/identity \
  -H 'Content-Type: application/json' \
  --data '{"type":"service_auth","login_hint":"person@example.com","client_name":"repository assistant","requested_scopes":["eval:read","runs:read"]}'
```

Keep the returned `claim_token` in memory. Give the person `claim.verification_uri`
and `claim.user_code`; they sign in, enter that code, select the accountable
organization, and approve or deny the exact scopes. Registration and its claim
code expire after ten minutes. No credential or identity assertion is returned
before approval. Poll at `claim.interval` seconds:

```bash
curl -X POST https://www.evalgate.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:workos:agent-auth:grant-type:claim' \
  --data-urlencode 'claim_token=<claim_token>'
```

Pending, denied, expired, and consumed claims return `authorization_pending`,
`access_denied`, `expired_token`, and `invalid_grant`. Honor `Retry-After` on
`slow_down`. Successful pickup happens once and returns an organization-scoped
Bearer `access_token`, approved `scope`, `expires_in`, a service-signed
`identity_assertion`, and `assertion_expires`.

To replace a pending user code, POST JSON `{"claim_token":"<claim_token>","email":"person@example.com"}`
to `/oauth/claim`. The email must match the original hint. The response's
`claim_attempt` contains the new code and verification URI; the old code is
invalidated and the original ten-minute expiry is unchanged. An expired
registration returns `410 claim_expired`; start again at `/oauth/identity`.
The verification URI contains a separate `claim_attempt_token`; keep that link
intact through sign-in. Both its random binding and the separately entered user
code are required for preview and approval. Renewal invalidates the old link as
well as the old code. The database stores hashes of both values.

While its assertion remains valid, a client can use the RFC 7523 grant:

```bash
curl -X POST https://www.evalgate.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode 'assertion=<identity_assertion>' \
  --data-urlencode 'resource=https://www.evalgate.com/api'
```

This returns the same approved credential, never an additional key or wider
grant. Each exchange rechecks revocation, expiry, the approving person's verified
email, current organization membership, and current scope ceiling. Assertions
expire within thirty minutes and never outlive the original ninety-day access
credential. They cannot extend its expiry. Revoke the access token through
`/oauth/revoke` or Developer → API Keys; its assertion then stops working too.
Keep both values in a secret store and never commit or log them.

The JWT-bearer grant accepts only EvalGate's own signed assertions from this
claim flow. There is no external ID-JAG trust list, anonymous pre-claim access,
or provider security-event receiver. The metadata omits those unsupported
identity types and endpoints.

## Pick a method

- **Person creating or joining an account:** use the interactive
  [GitHub or Google sign-in](https://www.evalgate.com/auth/sign-up). EvalGate
  does not ask for an email/password pair or send a separate EvalGate
  verification email.
- **OAuth device client:** use the advertised device-authorization and token
  endpoints with `client_id=evalgate-agent`. This is the `evalgate login` path
  and returns a `cli_session` after human approval.
- **OAuth MCP client:** dynamically register exact HTTPS or loopback redirect
  URIs at `/oauth/register`, then use `/oauth/authorize` with `response_type=code`,
  a protected `resource`, and PKCE `code_challenge_method=S256`.
- **Agent or automation using JSON:** start the equivalent short-lived
  credential handoff, ask an authorized organization admin to approve it after
  GitHub or Google sign-in, then use the API key returned once. An admin may
  still create a key manually.
- **Public discovery:** fetch only the operations that OpenAPI explicitly marks
  unauthenticated. Public discovery does not create an account or credential.

## Register / start authorization

EvalGate dynamically registers public clients; it never issues a client secret.
Redirect URIs are exact-match HTTPS URLs or loopback HTTP URLs without URL
credentials or fragments. Authorization codes expire after five minutes, are
single-use, and are bound to the client, redirect URI, protected resource, and
PKCE S256 challenge. The consent screen requires a signed-in person to select an
organization where their role can grant every requested scope.

EvalGate also exposes a non-browser device grant that preserves the same human
identity boundary.

Standards-based clients send an RFC 8628 form request:

```bash
curl -X POST https://www.evalgate.com/oauth/device/authorization \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=evalgate-agent' \
  --data-urlencode 'scope=eval:read runs:read traces:write'
```

The shared public client ID is a routing identifier, not client authentication
or proof of the requesting agent's identity. EvalGate persists the protocol
source and client ID so a JSON handoff cannot be exchanged through the OAuth
token endpoint, but the security boundary remains the displayed user code,
signed-in person, organization selection, and exact scope approval. Clients
that do not implement RFC 8628 may use the JSON adapter:

Start a request:

```bash
curl -X POST https://www.evalgate.com/api/agent-setup/start \
  -H 'Content-Type: application/json' \
  --data '{"client_name":"repository assistant","requested_scopes":["eval:read","runs:read","traces:write"]}'
```

The response contains a high-entropy `device_code`, a human-readable
`user_code`, `verification_uri`, ten-minute `expires_in`, and a minimum polling
`interval`. Show the person the verification URI and code. The person signs in
with GitHub or Google, creates or selects an organization, reviews every scope,
and approves or denies the request at `/agent/setup`.

Neither start endpoint creates an account, key, hosted evaluation, or billable
action. A credential is created only after a signed-in person approves the
organization and exact scopes.

## Claim / use the token endpoint

Authorization-code clients exchange the returned code with the same `client_id`,
exact `redirect_uri`, protected `resource`, and original `code_verifier`. A
successful exchange returns a 90-day organization-scoped OAuth-client bearer
credential.

RFC 8628 clients poll the advertised token endpoint no faster than the returned
interval:

```bash
curl -X POST https://www.evalgate.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
  --data-urlencode 'device_code=<device_code>' \
  --data-urlencode 'client_id=evalgate-agent'
```

The token endpoint returns standard `authorization_pending`, `slow_down`,
`access_denied`, `expired_token`, and `invalid_grant` errors. Approval returns
an opaque `access_token`, Bearer token type, approved scope string, and expiry.
For the device flow, that opaque token is a `cli_session`; its effective role is
resolved from the approving user's current organization membership.

JSON handoff clients poll the compatibility endpoint:

```bash
curl -X POST https://www.evalgate.com/api/agent-setup/poll \
  -H 'Content-Type: application/json' \
  --data '{"device_code":"<device_code>"}'
```

Pending requests return `202`. A client that polls too quickly receives `429`,
`Retry-After`, and an increased interval. Approval returns the bearer API key,
approved scopes, and expiry exactly once. Denied, expired, and consumed requests
never return a credential.

Anonymous or third-party identity-assertion registration is unsupported.
Organization membership and API-key issuance remain attributable to the
signed-in person who approved them. The `service_auth` profile above has a
separate source-bound claim grant; JSON handoff and RFC 8628 device codes cannot
be exchanged through that grant.

## Use the credential

Choose the least-privilege scopes required by the operation. Agent-facing
scopes include:

- The documentation MCP at `/api/mcp/docs` is public and reads only EvalGate's
  reviewed published-document catalog through tools and native resources. It
  has no organization context.
- `agent:read` — inspect A2A capabilities and task status.
- `agent:execute` — start a bounded A2A evaluation task.

Existing REST operations use the operation-specific scopes documented in
OpenAPI. API keys are organization-scoped; they do not supply model-provider
credentials or silently grant repository, billing, or administrative access.

For TypeScript CLI use, `evalgate login` starts a browser-approved device flow,
stores the resulting session outside the repository, and never prints the
credential. Then `evalgate link` establishes durable organization/repository/root
linkage and pins the activation snapshot to the exact local commit.
`evalgate status` reports identity, durable link, checkout relation, cloud
snapshot, local-gate readiness, and cloud-target readiness separately. A branch
moving ahead of the checkout does not unlink the repository, and EvalGate never
silently substitutes the branch head for the requested target SHA. The local session is
stored in `%APPDATA%\evalgate\config.json` on Windows,
`~/Library/Application Support/evalgate/config.json` on macOS, and
`${XDG_CONFIG_HOME:-~/.config}/evalgate/config.json` on Linux. An absolute
`EVALGATE_CONFIG_HOME` may override that directory in managed environments.
Repository configuration cannot supply a bearer key or redirect a saved session
to another API origin. Trusted alternate origins require HTTPS, except explicit
localhost and loopback development. CI and direct API clients should inject a
least-privilege `EVALGATE_API_KEY` through their secret store instead of saving a
local file.

```bash
curl https://www.evalgate.com/api/mcp \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Errors

- `401 Unauthorized` means the bearer key is missing, invalid, expired, or
  revoked. Read `WWW-Authenticate` and follow its `resource_metadata` link.
- `403 Forbidden` means the key is valid but lacks a required scope or
  organization permission. Request a narrower replacement key from an
  authorized organization member; do not retry with guessed scopes.
- `429 Too Many Requests` means the active rate policy was exceeded. Honor
  `Retry-After` and the advertised rate-limit headers before retrying.

Errors use the typed response shapes documented in OpenAPI.

## Revocation

An authorized organization member revokes an EvalGate API key under
**Developer → API Keys**. OAuth clients may also submit the opaque token to the
advertised `/oauth/revoke` endpoint. Revocation is idempotent and does not reveal
whether a submitted token was valid.
Callers must delete the revoked secret from CI and local secret stores, then use
a newly issued least-privilege key if access is still required.

## Safe test mode (not a hosted sandbox)

After approval and token pickup, an agent can verify the MCP transport without
provider calls or billable evaluation execution:

1. Call public MCP `server/discover` to verify protocol compatibility.
2. Call public schema-only `tools/list` to inspect the available contract, then
   authenticate before any `tools/call`.
3. Call read-only `project_plan` for a mutation-free setup plan.

Those three MCP operations create no model-provider credential, make no provider
call, persist no evaluation result, and cannot enable paid overage.

For local checks, `evalgate gate --offline` blocks EvalGate's built-in and
configured provider-backed checks unless network use is explicitly enabled.
Custom in-process evaluators remain ordinary project code and can still open a
network connection. Wrapped project commands receive hard network isolation
only on Linux when `unshare` is available and permitted; otherwise EvalGate
falls back to the `EVALGATE_OFFLINE=1` process contract. Neither path is
resettable hosted state or a production-like sandbox.

## Browser and billing boundary

No payment card is required for the free allowance. Interactive social sign-in,
organization creation, credential approval, and billing management remain
browser workflows for authorized people. Agents can start and poll the handoff
over HTTPS but cannot bypass that approval boundary.

The handoff codes are stored as hashes. An approved key is encrypted while it
awaits one-time delivery, is restricted to reviewed non-administrative scopes,
and expires after 90 days.

Last updated: 2026-10-21
