# auth.md

You are an agent. Streamloop supports standard OAuth 2.1 Authorization Code + PKCE after RFC 7591 public-client registration and explicit user consent. This is NOT the auth.md agent-identity protocol: Streamloop has no `identity_endpoint`, anonymous API identity, ID-JAG mint, or claim ceremony, so its authorization-server metadata intentionally omits `agent_auth.identity_types_supported`. Follow the standard OAuth metadata and steps below.

Placeholder hosts in examples: `https://mcp.streamloop.app/mcp` (the MCP resource server you want to call) and `https://auth.streamloop.app` (the authorization server that handles registration and tokens).

## Step 1 — Discover

Discovery is two hops; the 401 that pointed you here carries the PRM URL:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.streamloop.app/.well-known/oauth-protected-resource"
```

### 1a. Fetch the Protected Resource Metadata

```http
GET https://mcp.streamloop.app/.well-known/oauth-protected-resource
```

```json
{
  "resource": "https://mcp.streamloop.app/mcp",
  "authorization_servers": ["https://auth.streamloop.app"],
  "scopes_supported": ["streamloop:read", "streamloop:write", "streamloop:destructive", "streamloop:billing"],
  "bearer_methods_supported": ["header"]
}
```

- `resource` — the canonical URL of the MCP API. You MUST pass this as the `resource` (RFC 8707) parameter at the token endpoint so the issued token is a JWKS-verifiable JWT for this audience.
- `authorization_servers` — the Streamloop authorization server. Fetch its standard RFC 8414 metadata in Step 1b.

### 1b. Fetch the Authorization Server metadata

```http
GET https://auth.streamloop.app/.well-known/oauth-authorization-server
```

The response is standard RFC 8414 metadata:

```json
{
  "issuer": "https://auth.streamloop.app",
  "authorization_endpoint": "https://auth.streamloop.app/api/auth/oauth2/authorize",
  "token_endpoint": "https://auth.streamloop.app/api/auth/oauth2/token",
  "registration_endpoint": "https://auth.streamloop.app/api/auth/oauth2/register",
  "revocation_endpoint": "https://auth.streamloop.app/api/auth/oauth2/revoke",
  "jwks_uri": "https://auth.streamloop.app/api/auth/jwks",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "scopes_supported": ["openid", "profile", "email", "offline_access", "streamloop:read", "streamloop:write", "streamloop:destructive", "streamloop:billing"]
}
```

Streamloop does not advertise an auth.md `identity_endpoint` or identity type. Use the RFC 7591 `registration_endpoint` to register a public OAuth client, then obtain a user-consented credential through Authorization Code + PKCE. If no user can consent in a browser, the client cannot access Streamloop.

## Step 2 — Register a client (RFC 7591)

Self-register a public client. PKCE is mandatory; no client secret is issued for public clients.

```http
POST https://auth.streamloop.app/api/auth/oauth2/register
Content-Type: application/json

{
  "client_name": "My Agent",
  "redirect_uris": ["https://my-agent.example.com/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "openid profile email offline_access streamloop:read streamloop:write"
}
```

Response (201):

```json
{
  "client_id": "...",
  "redirect_uris": ["https://my-agent.example.com/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"]
}
```

Keep `client_id`. (If you are a confidential client, also keep `client_secret`.)

## Step 3 — Authorize with PKCE + user consent

Generate a PKCE verifier and `S256` challenge. Send the user's browser to the authorization endpoint, including the MCP `resource` (RFC 8707) so the resulting token is a JWT for that audience:

```http
GET https://auth.streamloop.app/api/auth/oauth2/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=https://my-agent.example.com/callback
  &scope=openid%20profile%20email%20offline_access%20streamloop:read%20streamloop:write
  &resource=https://mcp.streamloop.app/mcp
  &code_challenge=<S256-challenge>
  &code_challenge_method=S256
  &state=<random>
```

The user signs in (passwordless / Google / passkey) and is shown a consent screen listing the scopes you requested. **This is the user's consent gate** — surface the requested scopes to the user before you start this step. On approval, the browser is redirected to your `redirect_uri` with `?code=...&state=...`.

## Step 4 — Exchange the code for a token

```http
POST https://auth.streamloop.app/api/auth/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=https://my-agent.example.com/callback
&client_id=<client_id>
&code_verifier=<pkce-verifier>
&resource=https://mcp.streamloop.app/mcp
```

Response (200):

```json
{
  "access_token": "<JWT, ES256, aud=https://mcp.streamloop.app/mcp>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "<refresh token>",
  "scope": "openid profile email offline_access streamloop:read streamloop:write"
}
```

Because you sent the `resource`, `access_token` is a JWT verifiable against `jwks_uri` (ES256). Without `resource` it would be opaque and the MCP server would reject it.

## Step 5 — Use the credential

Call the MCP server with the access token as a bearer:

```http
POST https://mcp.streamloop.app/mcp
Authorization: Bearer <access_token>
```

- When `access_token` expires (15 min), refresh it: `POST /api/auth/oauth2/token` with `grant_type=refresh_token&refresh_token=<rt>&client_id=<client_id>&resource=https://mcp.streamloop.app/mcp`.
- On a 401 for a previously-working token: refresh once; if refresh fails, restart at Step 3 (the user may have revoked consent).

## Revocation (RFC 7009)

You revoke your own credential — there is no provider-push revocation. To sign out an agent:

```http
POST https://auth.streamloop.app/api/auth/oauth2/revoke
Content-Type: application/x-www-form-urlencoded

token=<refresh_token>
&token_type_hint=refresh_token
&client_id=<client_id>
```

Returns 200. Streamloop access tokens are short-lived JWTs and cannot be revoked before expiry; attempting to revoke one returns `unsupported_token_type`. After revoking the refresh token, the user must re-consent (Step 3) to re-authorize. The user can also revoke your delegation from the Streamloop dashboard at any time, after which your tokens stop working and you should restart at Step 1.

## What Streamloop does NOT support

To set expectations precisely (do not attempt these — the endpoints do not exist):

- No `POST /agent/auth` WorkOS-style registration endpoint.
- No ID-JAG identity-assertion grant and no provider trust list.
- No anonymous Streamloop API identity or anonymous API-key minting.
- No verified-email OTP claim ceremony.

Every API credential is a user-consented OAuth 2.1 delegation.
