Adds the full bedroom-developer-docs OpenSpec change implementation: - docs/developers/README.md — index page - docs/developers/quick-start.md — bootstrap to working token in 7 steps - docs/developers/concepts.md — AgentIdP, AGNTCY, lifecycle, OAuth 2.0, free tier - docs/developers/guides/README.md — guide index - docs/developers/guides/register-an-agent.md — all fields, validation, common errors - docs/developers/guides/manage-credentials.md — generate, list, rotate, revoke - docs/developers/guides/issue-and-revoke-tokens.md — OAuth 2.0 flow, introspect, revoke - docs/developers/guides/query-audit-logs.md — filters, pagination, 90-day retention - docs/developers/api-reference.md — all 14 endpoints, all error codes, curl examples Also commits deferred OpenSpec housekeeping from previous session: - Archives phase-1-mvp-implementation change to openspec/changes/archive/ - Adds bedroom-developer-docs change artifacts (30/30 tasks complete) - Syncs 4 delta specs to openspec/specs/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
204 lines
5.6 KiB
Markdown
204 lines
5.6 KiB
Markdown
# Issue and Revoke Tokens
|
|
|
|
This guide covers the complete token lifecycle: issuing, using, inspecting, and revoking JWT access tokens.
|
|
|
|
---
|
|
|
|
## Issue a token
|
|
|
|
`POST /api/v1/token`
|
|
|
|
This is the OAuth 2.0 Client Credentials grant. Your agent exchanges its `client_id` and `client_secret` for a signed JWT access token.
|
|
|
|
> **Important**: This endpoint uses `application/x-www-form-urlencoded` encoding, not JSON.
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:3000/api/v1/token \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "grant_type=client_credentials" \
|
|
-d "client_id=$CLIENT_ID" \
|
|
-d "client_secret=$CLIENT_SECRET" \
|
|
-d "scope=agents:read agents:write" | jq .
|
|
```
|
|
|
|
Response (`200 OK`):
|
|
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImp0aSI6InV1aWQtaGVyZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"scope": "agents:read agents:write"
|
|
}
|
|
```
|
|
|
|
The token expires in `3600` seconds (1 hour). Request a new one before it expires.
|
|
|
|
### Request fields
|
|
|
|
| Field | Required | Description |
|
|
|-------|----------|-------------|
|
|
| `grant_type` | Yes | Must be `client_credentials` |
|
|
| `client_id` | Yes | Your agent's `agentId` (UUID) |
|
|
| `client_secret` | Yes | The secret from credential generation |
|
|
| `scope` | No | Space-separated list of requested scopes. If omitted, all scopes are granted. |
|
|
|
|
### Available scopes
|
|
|
|
| Scope | What it allows |
|
|
|-------|----------------|
|
|
| `agents:read` | Read agent records |
|
|
| `agents:write` | Create, update, decommission agents |
|
|
| `tokens:read` | Introspect tokens |
|
|
| `audit:read` | Query audit logs |
|
|
|
|
Request only the scopes your agent needs.
|
|
|
|
---
|
|
|
|
## What's inside the JWT
|
|
|
|
A JWT has three base64-encoded parts separated by dots: header, payload, and signature. The payload contains your agent's identity claims.
|
|
|
|
Decode the payload to inspect it (for development only — never trust an unverified token in production):
|
|
|
|
```bash
|
|
# Extract the middle part (payload) of your token and decode it
|
|
TOKEN_PAYLOAD=$(echo "$TOKEN" | cut -d. -f2)
|
|
echo "$TOKEN_PAYLOAD" | base64 --decode 2>/dev/null | jq .
|
|
```
|
|
|
|
Claims in the payload:
|
|
|
|
| Claim | Description |
|
|
|-------|-------------|
|
|
| `sub` | Subject — your agent's `agentId` |
|
|
| `client_id` | The `agentId` that authenticated |
|
|
| `scope` | Scopes granted by this token |
|
|
| `jti` | JWT ID — unique identifier for this token (used for revocation) |
|
|
| `iat` | Issued at (Unix timestamp in seconds) |
|
|
| `exp` | Expires at (Unix timestamp in seconds) |
|
|
|
|
---
|
|
|
|
## Use the token
|
|
|
|
Include the token in the `Authorization` header of every API request:
|
|
|
|
```bash
|
|
curl -s http://localhost:3000/api/v1/agents \
|
|
-H "Authorization: Bearer $TOKEN" | jq .
|
|
```
|
|
|
|
---
|
|
|
|
## Introspect a token
|
|
|
|
`POST /api/v1/token/introspect`
|
|
|
|
Check whether a token is currently active (valid, not expired, not revoked). Requires a Bearer token with `tokens:read` scope.
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:3000/api/v1/token/introspect \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "token=$TOKEN_TO_CHECK" | jq .
|
|
```
|
|
|
|
Response for an active token:
|
|
|
|
```json
|
|
{
|
|
"active": true,
|
|
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"scope": "agents:read agents:write",
|
|
"token_type": "Bearer",
|
|
"iat": 1743151200,
|
|
"exp": 1743154800
|
|
}
|
|
```
|
|
|
|
Response for an inactive (expired or revoked) token:
|
|
|
|
```json
|
|
{
|
|
"active": false
|
|
}
|
|
```
|
|
|
|
> The introspect endpoint always returns `200 OK` — even for inactive tokens. You must check the `active` field to determine token validity.
|
|
|
|
---
|
|
|
|
## Revoke a token
|
|
|
|
`POST /api/v1/token/revoke`
|
|
|
|
Immediately invalidates a token, preventing it from being used for any subsequent requests. Requires a Bearer token.
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:3000/api/v1/token/revoke \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "token=$TOKEN_TO_REVOKE" | jq .
|
|
```
|
|
|
|
Response (`200 OK`):
|
|
|
|
```json
|
|
{}
|
|
```
|
|
|
|
**Notes on revocation**:
|
|
- Revocation is immediate — the token is rejected on the next request
|
|
- Revoking an already-revoked or expired token is not an error (idempotent per RFC 7009)
|
|
- An agent can revoke its own tokens; revoking another agent's token requires an admin-scoped token
|
|
- Revoking a token does not affect the credential that issued it — new tokens can still be obtained using the same credentials
|
|
|
|
---
|
|
|
|
## Token errors
|
|
|
|
### `401 invalid_client` — wrong credentials
|
|
|
|
```json
|
|
{
|
|
"error": "invalid_client",
|
|
"error_description": "Client authentication failed. Invalid client_id or client_secret."
|
|
}
|
|
```
|
|
|
|
Check that `client_id` matches the agent's `agentId` and `client_secret` is the current active secret.
|
|
|
|
### `403 unauthorized_client` — agent suspended or monthly limit reached
|
|
|
|
```json
|
|
{
|
|
"error": "unauthorized_client",
|
|
"error_description": "Agent is currently suspended and cannot obtain tokens."
|
|
}
|
|
```
|
|
|
|
Or:
|
|
|
|
```json
|
|
{
|
|
"error": "unauthorized_client",
|
|
"error_description": "Free tier monthly token limit of 10,000 requests has been reached."
|
|
}
|
|
```
|
|
|
|
For suspension: reactivate the agent first. For the monthly limit: the counter resets on the first day of the next calendar month.
|
|
|
|
### `400 unsupported_grant_type`
|
|
|
|
```json
|
|
{
|
|
"error": "unsupported_grant_type",
|
|
"error_description": "Only 'client_credentials' grant type is supported."
|
|
}
|
|
```
|
|
|
|
Only `client_credentials` is supported. Do not use `authorization_code`, `password`, or other grant types.
|