Neue Skills, Referenzen & OpenWiki-Doku integriert
Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
This commit is contained in:
403
.github/skills/auth-md/references/example-auth-md.md
vendored
Normal file
403
.github/skills/auth-md/references/example-auth-md.md
vendored
Normal file
@@ -0,0 +1,403 @@
|
||||
# Complete Example: auth.md
|
||||
|
||||
A working example of an `auth.md` file for the fictional "Acme Notes" service, updated to protocol v2 (June 2026).
|
||||
|
||||
---
|
||||
|
||||
## Example Context
|
||||
|
||||
- **Service:** Acme Notes (collaborative notes app)
|
||||
- **Resource Server:** `https://api.acmenotes.com`
|
||||
- **Auth Server:** `https://auth.acmenotes.com`
|
||||
- **Supported flows:** identity_assertion (ID-JAG), service_auth, anonymous
|
||||
- **Scopes:** `notes.read`, `notes.write`, `notes.admin`
|
||||
|
||||
---
|
||||
|
||||
## The File
|
||||
|
||||
```markdown
|
||||
# auth.md
|
||||
|
||||
You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API → handle revocation. Follow the steps in order; do not skip ahead.
|
||||
|
||||
The resource server is `https://api.acmenotes.com` and the authorization server is `https://auth.acmenotes.com`.
|
||||
|
||||
## Step 1 — Discover
|
||||
|
||||
Discovery is two hops. The 401 response that pointed you here carries a `WWW-Authenticate` header with the PRM URL:
|
||||
|
||||
\```http
|
||||
HTTP/1.1 401 Unauthorized
|
||||
WWW-Authenticate: Bearer resource_metadata="https://api.acmenotes.com/.well-known/oauth-protected-resource"
|
||||
\```
|
||||
|
||||
### 1a. Fetch the Protected Resource Metadata
|
||||
|
||||
\```http
|
||||
GET /.well-known/oauth-protected-resource
|
||||
\```
|
||||
|
||||
Response:
|
||||
|
||||
\```json
|
||||
{
|
||||
"resource": "https://api.acmenotes.com/",
|
||||
"resource_name": "Acme Notes",
|
||||
"resource_logo_uri": "https://acmenotes.com/logo.png",
|
||||
"authorization_servers": ["https://auth.acmenotes.com/"],
|
||||
"scopes_supported": ["notes.read", "notes.write", "notes.admin"],
|
||||
"bearer_methods_supported": ["header"]
|
||||
}
|
||||
\```
|
||||
|
||||
### 1b. Fetch the Authorization Server metadata
|
||||
|
||||
\```http
|
||||
GET /.well-known/oauth-authorization-server
|
||||
\```
|
||||
|
||||
Response:
|
||||
|
||||
\```json
|
||||
{
|
||||
"resource": "https://api.acmenotes.com/",
|
||||
"authorization_servers": ["https://auth.acmenotes.com/"],
|
||||
"scopes_supported": ["notes.read", "notes.write", "notes.admin"],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"issuer": "https://auth.acmenotes.com",
|
||||
"token_endpoint": "https://auth.acmenotes.com/oauth2/token",
|
||||
"revocation_endpoint": "https://auth.acmenotes.com/oauth2/revoke",
|
||||
"grant_types_supported": [
|
||||
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"urn:workos:agent-auth:grant-type:claim"
|
||||
],
|
||||
"agent_auth": {
|
||||
"skill": "https://acmenotes.com/auth.md",
|
||||
"identity_endpoint": "https://auth.acmenotes.com/agent/identity",
|
||||
"claim_endpoint": "https://auth.acmenotes.com/agent/identity/claim",
|
||||
"events_endpoint": "https://auth.acmenotes.com/agent/event/notify",
|
||||
"identity_types_supported": ["anonymous", "identity_assertion", "service_auth"],
|
||||
"identity_assertion": {
|
||||
"assertion_types_supported": [
|
||||
"urn:ietf:params:oauth:token-type:id-jag"
|
||||
]
|
||||
},
|
||||
"events_supported": [
|
||||
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
|
||||
]
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
## Step 2 — Pick a method
|
||||
|
||||
1. **You have a session tied to a user identity and can exchange it for an ID-JAG** → identity_assertion + id-jag.
|
||||
2. **You have only the user's email** → service_auth. Claim ceremony required.
|
||||
3. **You have neither** → anonymous. Claim ceremony optional.
|
||||
|
||||
Cross-check against the `agent_auth` block before proceeding.
|
||||
|
||||
## Step 3 — Register
|
||||
|
||||
Before sending an `identity_assertion` or `service_auth` body, surface "Acme Notes" and its logo to the user and confirm consent. Skip for `anonymous`.
|
||||
|
||||
### identity_assertion + id-jag
|
||||
|
||||
Mint the ID-JAG with:
|
||||
- `aud` = `https://api.acmenotes.com/` (from PRM `resource`)
|
||||
- `auth_time` = epoch seconds of user's last authentication at your provider (**required**)
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "identity_assertion",
|
||||
"assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
|
||||
"assertion": "<ID-JAG>"
|
||||
}
|
||||
\```
|
||||
|
||||
Response — no confirmation needed (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_01ABC123DEF456",
|
||||
"registration_type": "identity_assertion",
|
||||
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
|
||||
"assertion_expires": "2026-05-22T14:00:00.000Z",
|
||||
"scopes": ["notes.read", "notes.write"]
|
||||
}
|
||||
\```
|
||||
|
||||
Keep `identity_assertion` and go to Step 5.
|
||||
|
||||
Response — confirmation required (401, `interaction_required`):
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "interaction_required",
|
||||
"error_description": "ID-JAG email matches an existing account but no delegation on file for (iss, sub).",
|
||||
"registration_id": "reg_01ABC123DEF456",
|
||||
"registration_type": "identity_assertion",
|
||||
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
|
||||
"claim_token": "clm_xYz789AbC012dEf",
|
||||
"claim_token_expires": "2026-05-22T13:30:00.000Z",
|
||||
"post_claim_scopes": ["notes.read", "notes.write"],
|
||||
"claim": {
|
||||
"user_code": "847291",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_abc123",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
Surface `verification_uri` + `user_code` to the user (Step 4b) and poll (Step 4c).
|
||||
|
||||
Response — login required (401, `login_required`):
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "login_required",
|
||||
"error_description": "auth_time is 7200s old; max allowed is 3600s. Re-authenticate at the provider.",
|
||||
"max_age": 3600
|
||||
}
|
||||
\```
|
||||
|
||||
Re-authenticate the user at your provider and mint a fresh ID-JAG.
|
||||
|
||||
### service_auth
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "service_auth",
|
||||
"login_hint": "jane@example.com"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_01DEF789GHI012",
|
||||
"registration_type": "service_auth",
|
||||
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
|
||||
"claim_token": "clm_aBc123DeF456gHi",
|
||||
"claim_token_expires": "2026-05-22T13:30:00.000Z",
|
||||
"post_claim_scopes": ["notes.read", "notes.write"],
|
||||
"claim": {
|
||||
"user_code": "592841",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_def456",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
No `identity_assertion` yet. Go to Step 4.
|
||||
|
||||
### anonymous
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "anonymous"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_01GHI345JKL678",
|
||||
"registration_type": "anonymous",
|
||||
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
|
||||
"assertion_expires": "2026-05-22T14:00:00.000Z",
|
||||
"pre_claim_scopes": ["notes.read"],
|
||||
"claim_url": "https://auth.acmenotes.com/agent/identity/claim",
|
||||
"claim_token": "clm_mNo345PqR678sTu",
|
||||
"claim_token_expires": "2026-05-22T13:30:00.000Z",
|
||||
"post_claim_scopes": ["notes.read", "notes.write"]
|
||||
}
|
||||
\```
|
||||
|
||||
Exchange `identity_assertion` at `/oauth2/token` for an access_token with `pre_claim_scopes` (Step 5). To upgrade scopes, go to Step 4.
|
||||
|
||||
## Step 4 — Claim ceremony
|
||||
|
||||
### 4a. Get the ceremony materials
|
||||
|
||||
For **service_auth** and **interaction_required** responses, you already have the `claim` block. Skip to 4b.
|
||||
|
||||
For **anonymous**, initiate:
|
||||
|
||||
\```http
|
||||
POST /agent/identity/claim
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"claim_token": "clm_mNo345PqR678sTu",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_01GHI345JKL678",
|
||||
"claim_attempt_id": "cla_001",
|
||||
"status": "initiated",
|
||||
"expires_at": "2026-05-22T13:40:00.000Z",
|
||||
"claim_attempt": {
|
||||
"user_code": "374918",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
### 4b. Hand off to the user
|
||||
|
||||
Surface to the user:
|
||||
|
||||
> Open this link, sign in (or sign up), and enter this 6-digit code: **374918**
|
||||
> https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789
|
||||
|
||||
The user signs in to Acme Notes, sees a confirmation page, and types the code.
|
||||
|
||||
### 4c. Poll for completion
|
||||
|
||||
\```http
|
||||
POST /oauth2/token
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_mNo345PqR678sTu
|
||||
\```
|
||||
|
||||
Response while waiting:
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "authorization_pending",
|
||||
"error_description": "User has not completed the claim ceremony yet."
|
||||
}
|
||||
\```
|
||||
|
||||
Response on success:
|
||||
|
||||
\```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "notes.read notes.write",
|
||||
"identity_assertion": "eyJhbGciOiJFUzI1NiJ9...",
|
||||
"assertion_expires": "2026-05-22T15:00:00.000Z"
|
||||
}
|
||||
\```
|
||||
|
||||
Use `access_token` immediately. Cache `identity_assertion` for refresh via Step 5.
|
||||
|
||||
If `user_code` window expires:
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "expired_token",
|
||||
"error_description": "The user_code window has closed."
|
||||
}
|
||||
\```
|
||||
|
||||
Re-call `POST /agent/identity/claim` with same `claim_token` and `email` for a fresh code. If that returns `claim_expired`, restart at Step 3.
|
||||
|
||||
## Step 5 — Exchange the assertion
|
||||
|
||||
\```http
|
||||
POST /oauth2/token
|
||||
Host: auth.acmenotes.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJhbGciOiJFUzI1NiJ9...&resource=https://api.acmenotes.com/
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "notes.read notes.write"
|
||||
}
|
||||
\```
|
||||
|
||||
Same `identity_assertion` can be re-used until it expires. If `invalid_grant`, restart at Step 3.
|
||||
|
||||
## Step 6 — Use the access_token
|
||||
|
||||
\```http
|
||||
GET /api/notes
|
||||
Host: api.acmenotes.com
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
|
||||
\```
|
||||
|
||||
**Refresh:** When access_token expires, re-call Step 5 with same identity_assertion. When identity_assertion expires or Step 5 returns `invalid_grant`, restart at Step 3.
|
||||
|
||||
On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1.
|
||||
|
||||
Full API docs: https://docs.acmenotes.com/
|
||||
|
||||
## Errors
|
||||
|
||||
| Code | Where | What to do |
|
||||
|------|-------|------------|
|
||||
| `anonymous_not_enabled` | `/agent/identity` | Pick another method from Step 2 |
|
||||
| `service_auth_not_enabled` | `/agent/identity` | Pick another method |
|
||||
| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list. Pick another method |
|
||||
| `invalid_request` | `/agent/identity` | Fix body shape, ID-JAG signature/jti/aud problems |
|
||||
| `interaction_required` (401) | `/agent/identity` (ID-JAG) | Body carries `claim` block; surface to user (Step 4) |
|
||||
| `login_required` (401) | `/agent/identity` (ID-JAG) | Re-authenticate user at provider, mint fresh ID-JAG |
|
||||
| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3 |
|
||||
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed |
|
||||
| `claim_expired` | `/agent/identity/claim` | Restart at Step 3 |
|
||||
| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 |
|
||||
| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grants |
|
||||
| `authorization_pending` | `/oauth2/token` (claim) | User hasn't finished. Honor `interval` |
|
||||
| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart |
|
||||
| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval |
|
||||
| `rate_limited` (429) | any | Back off and retry |
|
||||
|
||||
## Revocation
|
||||
|
||||
Two independent layers:
|
||||
|
||||
- **Credential layer (RFC 7009):** POST `token=<access_token>&token_type_hint=access_token` to `https://auth.acmenotes.com/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5.
|
||||
- **Registration layer (RFC 8935):** Provider POSTs a Security Event Token to `events_endpoint`. Invalidates identity_assertion and all derived access_tokens. Agent discovers via `invalid_grant` at `/oauth2/token` — restart at Step 3.
|
||||
|
||||
On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation. If `invalid_grant`, registration-layer — restart at Step 3.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes on This Example
|
||||
|
||||
1. **All three flows** documented — in production, delete sections for unsupported flows
|
||||
2. **Token exchange step** (Step 5) — registration never returns access_token directly; always returns identity_assertion
|
||||
3. **Browser-based claim ceremony** — user_code + verification_uri replaces OTP-via-email
|
||||
4. **`interaction_required` response** — shows the 401 path when ID-JAG email matches existing account without delegation
|
||||
5. **`login_required` response** — shows `auth_time` freshness enforcement
|
||||
6. **Two revocation layers** — credential (agent-callable) vs registration (provider-driven)
|
||||
7. **Claim polling at `/oauth2/token`** — uses profile-specific grant URN to avoid collision with standard RFC 8628
|
||||
433
.github/skills/auth-md/references/implementation-guide.md
vendored
Normal file
433
.github/skills/auth-md/references/implementation-guide.md
vendored
Normal file
@@ -0,0 +1,433 @@
|
||||
# Server-Side Implementation Guide
|
||||
|
||||
Detailed guidance for implementing auth.md protocol endpoints on the backend (v2, June 2026). Covers discovery, registration, claim ceremony, token exchange, revocation, security, rate limiting, and audit events.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Minimum Implementation](#minimum-implementation)
|
||||
2. [Discovery Documents](#discovery-documents)
|
||||
3. [POST /agent/identity — Registration Handler](#post-agentidentity--registration-handler)
|
||||
4. [ID-JAG Verification](#id-jag-verification)
|
||||
5. [Claim Ceremony](#claim-ceremony)
|
||||
6. [POST /oauth2/token — Token Endpoint](#post-oauth2token--token-endpoint)
|
||||
7. [Revocation](#revocation)
|
||||
8. [User Matching and JIT Provisioning](#user-matching-and-jit-provisioning)
|
||||
9. [Rate Limiting](#rate-limiting)
|
||||
10. [Security](#security)
|
||||
11. [Audit Events](#audit-events)
|
||||
12. [Deploy Checklist](#deploy-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Minimum Implementation
|
||||
|
||||
1. Publish `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri`
|
||||
2. Publish `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block
|
||||
3. Return `WWW-Authenticate: Bearer resource_metadata="..."` on 401 responses
|
||||
4. Host `POST /agent/identity` that dispatches on the `type` field
|
||||
5. For identity_assertion: maintain a trust list and verify ID-JAG signatures via JWKS, validate `auth_time`
|
||||
6. For service_auth: return `claim` block with `user_code` + `verification_uri`
|
||||
7. For anonymous: issue `identity_assertion` immediately with pre-claim scopes
|
||||
8. Host `POST /agent/identity/claim` for deferred claim initiation
|
||||
9. Implement `POST /oauth2/token` handling both grant types (jwt-bearer exchange + claim polling)
|
||||
10. Implement `POST /oauth2/revoke` for credential-layer revocation (RFC 7009)
|
||||
11. Accept SETs at `events_endpoint` for registration-layer revocation (RFC 8935)
|
||||
12. Record audit events for every state change
|
||||
|
||||
---
|
||||
|
||||
## Discovery Documents
|
||||
|
||||
### Serving the PRM
|
||||
|
||||
```
|
||||
GET /.well-known/oauth-protected-resource
|
||||
→ 200 OK
|
||||
→ Content-Type: application/json
|
||||
```
|
||||
|
||||
Must include `resource_name` and `resource_logo_uri` — agents surface these to the user for consent before asserting identity.
|
||||
|
||||
Cache aggressively: `Cache-Control: public, max-age=3600`.
|
||||
|
||||
### Serving the AS Metadata
|
||||
|
||||
```
|
||||
GET /.well-known/oauth-authorization-server
|
||||
→ 200 OK
|
||||
→ Content-Type: application/json
|
||||
```
|
||||
|
||||
Must include standard OAuth fields (`issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`) plus the `agent_auth` block with `identity_endpoint`, `claim_endpoint`, `events_endpoint`.
|
||||
|
||||
### WWW-Authenticate Header
|
||||
|
||||
On every 401 response from the API:
|
||||
|
||||
```http
|
||||
HTTP/1.1 401 Unauthorized
|
||||
WWW-Authenticate: Bearer resource_metadata="https://api.service.com/.well-known/oauth-protected-resource"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /agent/identity — Registration Handler
|
||||
|
||||
All registration requests share the same endpoint and dispatch on the `type` field:
|
||||
|
||||
```
|
||||
POST /agent/identity
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Dispatch
|
||||
|
||||
| `type` | Flow | Returns |
|
||||
|--------|------|---------|
|
||||
| `identity_assertion` | ID-JAG verified | `identity_assertion` (service-signed JWT) |
|
||||
| `service_auth` | Email hint + browser ceremony | `claim` block (ceremony materials) |
|
||||
| `anonymous` | No identity | `identity_assertion` + `claim_token` for deferred claim |
|
||||
|
||||
### Handler: identity_assertion + id-jag
|
||||
|
||||
1. Decode the ID-JAG header to obtain `kid` and `alg`
|
||||
2. Look up the issuer (`iss`) in the trust list. Reject if unknown → `issuer_not_enabled`
|
||||
3. Fetch JWKS from the provider (see ID-JAG Verification section for caching)
|
||||
4. Verify signature → `invalid_request` if fails
|
||||
5. Validate claims:
|
||||
- `aud` matches the `resource` from PRM → `invalid_request`
|
||||
- `exp` is in the future → `invalid_request`
|
||||
- `iat` not unreasonably in the future (~1-2 min skew)
|
||||
- `jti` not seen recently → `invalid_request` (replay)
|
||||
- `auth_time` present and within `idJagMaxAuthAgeSeconds` → `login_required` (401) if too old
|
||||
- At least `email_verified` or `phone_number_verified` is `true` → `invalid_request`
|
||||
6. Match or provision the user (see User Matching)
|
||||
7. Check delegation: if `(iss, sub)` is known OR JIT-provisioned without collision → success
|
||||
8. If email/phone collision with existing account but no delegation on file → `interaction_required` (401) with `claim` block
|
||||
9. On success: sign an `identity_assertion` JWT and return it
|
||||
|
||||
### Handler: service_auth
|
||||
|
||||
1. Validate `login_hint` (email format)
|
||||
2. Create a registration row with type `service_auth`
|
||||
3. Generate `claim_token` (returned to agent once), `user_code` (6-digit), `claim_attempt_token` (embedded in verification_uri)
|
||||
4. Store SHA-256 hashes of `claim_token`
|
||||
5. Build `verification_uri` pointing to the service's login page with `return_to` parameter
|
||||
6. Return the registration response with `claim` block containing `user_code`, `verification_uri`, `expires_in`, `interval`
|
||||
|
||||
### Handler: anonymous
|
||||
|
||||
1. Apply rate limits
|
||||
2. Create a registration row
|
||||
3. Sign an `identity_assertion` JWT with pre-claim scopes
|
||||
4. Generate `claim_token` for deferred claim. Store only SHA-256 hash.
|
||||
5. Return `identity_assertion` + `claim_token` + pre/post claim scopes
|
||||
|
||||
---
|
||||
|
||||
## ID-JAG Verification
|
||||
|
||||
### Trust List
|
||||
|
||||
Maintain a registry of providers. Minimum entry: issuer URL. Richer entries can pin JWKS URI, CIMD URL, or attestation policy.
|
||||
|
||||
### JWKS Fetching
|
||||
|
||||
- Fetch `{iss}/.well-known/jwks.json` on first use
|
||||
- Cache per `Cache-Control`, with floor 10 min, ceiling 24h
|
||||
- On `kid` miss, refetch once before rejecting
|
||||
|
||||
### CIMD Resolution
|
||||
|
||||
If `client_id` is a URL:
|
||||
1. Fetch as OAuth Client ID Metadata Document
|
||||
2. Verify `jwks_uri` matches the one used to verify signature
|
||||
|
||||
### auth_time Validation
|
||||
|
||||
- `auth_time` is **required** in ID-JAGs
|
||||
- Compare `now() - auth_time` against `idJagMaxAuthAgeSeconds` (service-configured, e.g., 3600)
|
||||
- If too old: return `login_required` (401) with `max_age` in the response
|
||||
- Agent must re-authenticate user at provider and mint fresh ID-JAG
|
||||
|
||||
### Replay Protection
|
||||
|
||||
- Cache `jti` values with TTL of at least `exp - iat` + clock skew (typically 6 min)
|
||||
- Reject on collision with `invalid_request`
|
||||
|
||||
---
|
||||
|
||||
## Claim Ceremony
|
||||
|
||||
The v2 claim ceremony is browser-based, borrowing from RFC 8628 device authorization.
|
||||
|
||||
### POST /agent/identity/claim (anonymous deferred claim)
|
||||
|
||||
1. Hash `claim_token`, look up registration
|
||||
2. Reject if not found → `invalid_claim_token`, already claimed → `claimed_or_in_flight`, expired → `claim_expired`
|
||||
3. Generate `user_code` (6-digit), `claim_attempt_token`
|
||||
4. Build `verification_uri` with embedded `claim_attempt_token`
|
||||
5. Return `claim_attempt` block with `user_code`, `verification_uri`, `expires_in`, `interval`
|
||||
|
||||
### Service-Hosted Claim Page
|
||||
|
||||
When the user opens `verification_uri`:
|
||||
1. Redirect to login if not authenticated
|
||||
2. After login, show claim page displaying:
|
||||
- The user's identity ("You're signed in as jane@example.com")
|
||||
- The requesting agent/provider info (from PRM `resource_name`)
|
||||
- Input field for the 6-digit code
|
||||
3. On correct code submission: mark claim as complete, associate registration with user
|
||||
4. Incorrect code: show error, allow retry (up to limit)
|
||||
|
||||
### Claim Completion Flow
|
||||
|
||||
When the user submits the correct `user_code` on the claim page:
|
||||
1. Mark the claim as complete in the database
|
||||
2. For anonymous: sign a new `identity_assertion` (v2) carrying user claims, superseding the pre-claim one
|
||||
3. For service_auth: sign the first `identity_assertion` for this registration
|
||||
4. The next poll at `/oauth2/token` with the claim grant returns the access_token + identity_assertion
|
||||
|
||||
---
|
||||
|
||||
## POST /oauth2/token — Token Endpoint
|
||||
|
||||
Handles two grant types at the same endpoint:
|
||||
|
||||
### Grant: urn:ietf:params:oauth:grant-type:jwt-bearer (Token Exchange)
|
||||
|
||||
```
|
||||
POST /oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
&assertion=<identity_assertion>
|
||||
&resource=<resource_url> (optional but recommended)
|
||||
```
|
||||
|
||||
Processing:
|
||||
1. Verify the `identity_assertion` JWT signature (service's own key)
|
||||
2. Validate `exp` not passed → `invalid_grant` if expired
|
||||
3. Check assertion not revoked → `invalid_grant` if revoked
|
||||
4. Mint a fresh `access_token` scoped to the assertion's scopes
|
||||
5. Return standard OAuth token response
|
||||
|
||||
### Grant: urn:workos:agent-auth:grant-type:claim (Claim Polling)
|
||||
|
||||
```
|
||||
POST /oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:workos:agent-auth:grant-type:claim
|
||||
&claim_token=<claim_token>
|
||||
```
|
||||
|
||||
Processing:
|
||||
1. Hash `claim_token`, look up registration
|
||||
2. If claim not yet completed → return `authorization_pending`
|
||||
3. If user_code window expired → return `expired_token`
|
||||
4. If polling too fast → return `slow_down`
|
||||
5. If claim completed → mint access_token + return along with new `identity_assertion`
|
||||
|
||||
Why a profile-specific grant URN? So this doesn't collide with services that also implement standard RFC 8628 device authorization at the same token endpoint.
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Error | HTTP | Condition |
|
||||
|-------|------|-----------|
|
||||
| `invalid_grant` | 400 | Assertion expired, revoked, or invalid |
|
||||
| `invalid_client` | 401 | client_id not recognized |
|
||||
| `unsupported_grant_type` | 400 | Not one of the two supported grants |
|
||||
| `authorization_pending` | 400 | Claim polling — user hasn't finished |
|
||||
| `expired_token` | 400 | user_code window closed |
|
||||
| `slow_down` | 400 | Polling too fast — add ≥5s |
|
||||
|
||||
---
|
||||
|
||||
## Revocation
|
||||
|
||||
### Credential Layer — POST /oauth2/revoke (RFC 7009)
|
||||
|
||||
Agent-callable. Kills one access_token.
|
||||
|
||||
```
|
||||
POST /oauth2/revoke
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
token=<access_token>&token_type_hint=access_token
|
||||
```
|
||||
|
||||
- Return 200 on success (idempotent)
|
||||
- The underlying `identity_assertion` remains valid — agent can re-exchange for a new access_token
|
||||
|
||||
### Registration Layer — Events Endpoint (RFC 8935)
|
||||
|
||||
Provider-driven. Receives Security Event Tokens.
|
||||
|
||||
```
|
||||
POST /agent/event/notify
|
||||
Content-Type: application/secevent+jwt
|
||||
|
||||
<SET JWT>
|
||||
```
|
||||
|
||||
Processing:
|
||||
1. Verify SET signature against issuer's JWKS
|
||||
2. Enforce `jti` uniqueness
|
||||
3. Match `events` key to supported schemas
|
||||
4. For `https://schemas.workos.com/events/agent/auth/identity/assertion/revoked`:
|
||||
- Invalidate all identity_assertions for `(iss, sub, aud)`
|
||||
- Invalidate all derived access_tokens
|
||||
5. Return 200 on success, 400 on verification failure
|
||||
|
||||
### Bulk Revocation
|
||||
|
||||
Provide an operator-facing mechanism to revoke all outstanding identity_assertions and access_tokens for a tenant in one shot — for incident response.
|
||||
|
||||
---
|
||||
|
||||
## User Matching and JIT Provisioning
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. **Delegation record match** — if `(iss, sub)` has a delegation on file, route to that user. Strongest identifier.
|
||||
2. **Verified email match** — if a user exists with same verified email:
|
||||
- If delegation exists for `(iss, sub)` → direct match (already covered above)
|
||||
- If NO delegation for `(iss, sub)` → `interaction_required` (401). User must confirm linking.
|
||||
3. **Verified phone match** — same pattern.
|
||||
4. **No match → JIT** — create a new user per provisioning policy, or refuse.
|
||||
|
||||
Reject ID-JAGs with neither verified email nor verified phone.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Two Tiers
|
||||
|
||||
| Tier | Checked | Default Anonymous | Default identity_assertion |
|
||||
|------|---------|-------------------|---------------------------|
|
||||
| Per-IP | First | 5/hour | 60/hour |
|
||||
| Per-tenant | Second | 100/hour | 1000/hour |
|
||||
|
||||
### Implementation
|
||||
|
||||
- Sliding-window counter with shared store
|
||||
- Fail open on store errors
|
||||
- Return 429 with `Retry-After` header
|
||||
- Also rate-limit `/oauth2/token` polling (respect `interval` from claim block, reject with `slow_down`)
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Token Hashing
|
||||
|
||||
| Token | Storage | Plaintext leaves server |
|
||||
|-------|---------|------------------------|
|
||||
| `claim_token` | SHA-256 hash | Once, in the registration response |
|
||||
| `user_code` | Stored for comparison | Displayed on claim page when user submits |
|
||||
| `identity_assertion` | Full JWT stored (or just signature hash for lookup) | In registration response |
|
||||
|
||||
### claim_token
|
||||
|
||||
- Returned **exactly once** to the agent in the registration response
|
||||
- Agent holds in memory for ceremony duration — must not persist past Step 4
|
||||
- High-entropy: prefix `clm_` + 25+ chars base62
|
||||
|
||||
### auth_time Enforcement
|
||||
|
||||
- Service configures `idJagMaxAuthAgeSeconds` (default: 3600)
|
||||
- Reject ID-JAGs where `now() - auth_time > idJagMaxAuthAgeSeconds`
|
||||
- Return `login_required` (401) with `max_age` field
|
||||
|
||||
### Consent UX
|
||||
|
||||
- Surface `resource_name` and `resource_logo_uri` from PRM to user before identity assertion
|
||||
- The claim page should display who is requesting access (provider name from CIMD or ID-JAG metadata)
|
||||
|
||||
### user_code Security
|
||||
|
||||
- 6-digit numeric code
|
||||
- 10-minute TTL (configurable via `expires_in`)
|
||||
- Tight retry limits (3-5 attempts) on the claim page
|
||||
- Code is tied to the `claim_attempt_token` embedded in `verification_uri`
|
||||
|
||||
### Replay Protection
|
||||
|
||||
- `jti` cache mandatory for ID-JAGs
|
||||
- `jti` uniqueness enforced for SETs at events_endpoint
|
||||
- Shared store required for multi-replica deployments
|
||||
|
||||
### Trust List Discipline
|
||||
|
||||
Treat the trusted-providers list as security-critical configuration. Changes should be audited.
|
||||
|
||||
---
|
||||
|
||||
## Audit Events
|
||||
|
||||
### Recommended Events
|
||||
|
||||
| Event | When | Minimum Data |
|
||||
|-------|------|--------------|
|
||||
| `registration.created` | Successful POST /agent/identity | registration_id, registration_type, iss, sub (if ID-JAG) |
|
||||
| `registration.interaction_required` | 401 interaction_required returned | registration_id, iss, sub, matched_user_id |
|
||||
| `registration.login_required` | 401 login_required returned | iss, sub, auth_time, max_age |
|
||||
| `claim.initiated` | /agent/identity/claim called | registration_id, email |
|
||||
| `claim.completed` | User submitted correct user_code | registration_id, claimed_by_user_id |
|
||||
| `claim.expired` | user_code window or registration expired | registration_id |
|
||||
| `token.exchanged` | /oauth2/token jwt-bearer success | registration_id, access_token_id |
|
||||
| `token.revoked` | /oauth2/revoke called | access_token_id |
|
||||
| `registration.revoked` | SET processed at events_endpoint | registration_id, iss, sub |
|
||||
| `registration.expired` | Unclaimed registration past TTL | registration_id |
|
||||
|
||||
---
|
||||
|
||||
## Deploy Checklist
|
||||
|
||||
### Before publishing
|
||||
|
||||
- [ ] PRM served at `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri`
|
||||
- [ ] AS metadata served at `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block
|
||||
- [ ] `agent_auth` contains `identity_endpoint`, `claim_endpoint`, `events_endpoint`
|
||||
- [ ] `auth.md` served at the domain root
|
||||
- [ ] API returns `WWW-Authenticate` header on 401s
|
||||
- [ ] `POST /agent/identity` dispatches correctly by `type`
|
||||
- [ ] Trust list configured (if identity_assertion)
|
||||
- [ ] JWKS fetching with cache (if identity_assertion)
|
||||
- [ ] `auth_time` validation against `idJagMaxAuthAgeSeconds` (if identity_assertion)
|
||||
- [ ] `POST /agent/identity/claim` generates user_code + verification_uri (if service_auth/anonymous)
|
||||
- [ ] Claim page served at verification_uri (login → code input → confirm)
|
||||
- [ ] `POST /oauth2/token` handles jwt-bearer grant (assertion → access_token)
|
||||
- [ ] `POST /oauth2/token` handles claim grant (polling → access_token + identity_assertion)
|
||||
- [ ] `POST /oauth2/revoke` kills access_tokens (RFC 7009)
|
||||
- [ ] Events endpoint accepts SETs for registration revocation
|
||||
- [ ] Rate limiting active on `/agent/identity` and `/oauth2/token`
|
||||
- [ ] claim_token stored as SHA-256 hash
|
||||
- [ ] Replay protection for `jti` implemented
|
||||
- [ ] Audit events being recorded
|
||||
|
||||
### Recommended tests
|
||||
|
||||
- [ ] identity_assertion: valid ID-JAG with fresh auth_time → identity_assertion returned
|
||||
- [ ] identity_assertion: expired ID-JAG → `invalid_request`
|
||||
- [ ] identity_assertion: auth_time too old → `login_required` (401)
|
||||
- [ ] identity_assertion: email collision without delegation → `interaction_required` (401) with claim block
|
||||
- [ ] identity_assertion: repeated `jti` → `invalid_request` (replay)
|
||||
- [ ] identity_assertion: unknown issuer → `issuer_not_enabled`
|
||||
- [ ] service_auth: valid email → registration with claim block (user_code + verification_uri)
|
||||
- [ ] anonymous: registration → identity_assertion with pre_claim_scopes
|
||||
- [ ] anonymous: claim initiation → claim_attempt with user_code
|
||||
- [ ] /oauth2/token jwt-bearer: valid assertion → access_token
|
||||
- [ ] /oauth2/token jwt-bearer: expired assertion → `invalid_grant`
|
||||
- [ ] /oauth2/token claim: before completion → `authorization_pending`
|
||||
- [ ] /oauth2/token claim: after completion → access_token + identity_assertion
|
||||
- [ ] /oauth2/token claim: after window expires → `expired_token`
|
||||
- [ ] /oauth2/token claim: too-fast polling → `slow_down`
|
||||
- [ ] /oauth2/revoke: valid access_token → 200
|
||||
- [ ] events_endpoint: valid SET → registrations revoked
|
||||
- [ ] Rate limit exceeded → 429
|
||||
- [ ] 401 on API → WWW-Authenticate header present
|
||||
370
.github/skills/auth-md/references/metadata-schema.md
vendored
Normal file
370
.github/skills/auth-md/references/metadata-schema.md
vendored
Normal file
@@ -0,0 +1,370 @@
|
||||
# Metadata Schema
|
||||
|
||||
JSON structure reference for the discovery documents and tokens required by the auth.md protocol (v2, June 2026).
|
||||
|
||||
---
|
||||
|
||||
## Protected Resource Metadata (PRM)
|
||||
|
||||
Served at: `{resource_server}/.well-known/oauth-protected-resource`
|
||||
|
||||
Defined by [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728).
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "https://api.example.com/",
|
||||
"resource_name": "Example Service",
|
||||
"resource_logo_uri": "https://example.com/logo.png",
|
||||
"authorization_servers": ["https://auth.example.com/"],
|
||||
"scopes_supported": ["read", "write", "admin"],
|
||||
"bearer_methods_supported": ["header"]
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `resource` | ✅ | string (URL) | Canonical URL of the API. Used as `aud` in ID-JAGs. |
|
||||
| `resource_name` | ✅ | string | Display name for consent prompts. Surface to user before asserting identity. |
|
||||
| `resource_logo_uri` | Recommended | string (URL) | Logo for consent UI. Surface alongside `resource_name`. |
|
||||
| `authorization_servers` | ✅ | string[] | Base URLs of OAuth AS(s). Agent fetches AS metadata from here. |
|
||||
| `scopes_supported` | ✅ | string[] | All scopes the resource server understands. |
|
||||
| `bearer_methods_supported` | ✅ | string[] | How credentials are presented. Typically `["header"]`. |
|
||||
|
||||
---
|
||||
|
||||
## Authorization Server Metadata
|
||||
|
||||
Served at: `{authorization_server}/.well-known/oauth-authorization-server`
|
||||
|
||||
Combines standard [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) fields with the `agent_auth` profile block.
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "https://api.example.com/",
|
||||
"authorization_servers": ["https://auth.example.com/"],
|
||||
"scopes_supported": ["read", "write", "admin"],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"issuer": "https://auth.example.com",
|
||||
"token_endpoint": "https://auth.example.com/oauth2/token",
|
||||
"revocation_endpoint": "https://auth.example.com/oauth2/revoke",
|
||||
"grant_types_supported": [
|
||||
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"urn:workos:agent-auth:grant-type:claim"
|
||||
],
|
||||
"agent_auth": {
|
||||
"skill": "https://example.com/auth.md",
|
||||
"identity_endpoint": "https://auth.example.com/agent/identity",
|
||||
"claim_endpoint": "https://auth.example.com/agent/identity/claim",
|
||||
"events_endpoint": "https://auth.example.com/agent/event/notify",
|
||||
"identity_types_supported": ["anonymous", "identity_assertion", "service_auth"],
|
||||
"identity_assertion": {
|
||||
"assertion_types_supported": [
|
||||
"urn:ietf:params:oauth:token-type:id-jag"
|
||||
]
|
||||
},
|
||||
"events_supported": [
|
||||
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Top-Level OAuth Fields
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `issuer` | ✅ | string (URL) | Canonical issuer URL of this AS. Validate `iss` claim of any token the AS signs against this. |
|
||||
| `token_endpoint` | ✅ | string (URL) | Where agents exchange identity assertions for access_tokens (Step 5) and poll during claim ceremony (Step 4c). |
|
||||
| `revocation_endpoint` | ✅ | string (URL) | Where agents POST to revoke an access_token ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). |
|
||||
| `grant_types_supported` | ✅ | string[] | Grant types accepted at `token_endpoint`. Must include `urn:ietf:params:oauth:grant-type:jwt-bearer` (token exchange) and `urn:workos:agent-auth:grant-type:claim` (claim polling). |
|
||||
|
||||
### `agent_auth` Block Fields
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `skill` | Recommended | string (URL) | URL of the auth.md file. |
|
||||
| `identity_endpoint` | ✅ | string (URL) | POST endpoint for registration (Step 3). |
|
||||
| `claim_endpoint` | Conditional | string (URL) | POST endpoint for claim initiation. Required if `service_auth` or `anonymous` supported. |
|
||||
| `events_endpoint` | Recommended | string (URL) | Receives Security Event Tokens ([RFC 8417](https://datatracker.ietf.org/doc/html/rfc8417)) from providers for registration-layer revocation. |
|
||||
| `identity_types_supported` | ✅ | string[] | `"anonymous"`, `"identity_assertion"`, `"service_auth"`, or combination. |
|
||||
| `identity_assertion` | Conditional | object | Required if `"identity_assertion"` in identity_types_supported. |
|
||||
| `identity_assertion.assertion_types_supported` | ✅ (if identity_assertion) | string[] | `"urn:ietf:params:oauth:token-type:id-jag"`. |
|
||||
| `events_supported` | Recommended | string[] | Event schemas this service can ingest (currently revocation). Informational. |
|
||||
|
||||
### identity_types → Flow Mapping
|
||||
|
||||
| `identity_types_supported` | Flow | Ceremony |
|
||||
|---|---|---|
|
||||
| `identity_assertion` | ID-JAG verified by provider | None (unless `interaction_required`) |
|
||||
| `service_auth` | Email hint, browser-based ceremony | RFC 8628-style: user_code + verification_uri |
|
||||
| `anonymous` | No identity upfront | Optional deferred claim |
|
||||
|
||||
---
|
||||
|
||||
## ID-JAG Token (Identity Assertion JWT Authorization Grant)
|
||||
|
||||
Defined by [draft-ietf-oauth-identity-assertion-authz-grant](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant).
|
||||
|
||||
### Header
|
||||
|
||||
```json
|
||||
{
|
||||
"typ": "oauth-id-jag+jwt",
|
||||
"alg": "ES256",
|
||||
"kid": "<provider-key-id>"
|
||||
}
|
||||
```
|
||||
|
||||
### Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://api.agent-provider.com",
|
||||
"sub": "<opaque-user-identifier>",
|
||||
"aud": "https://api.example.com",
|
||||
"client_id": "<issuer-url-or-cimd-url>",
|
||||
"jti": "<unique-token-id>",
|
||||
"iat": 1716400000,
|
||||
"exp": 1716400300,
|
||||
"auth_time": 1716399000,
|
||||
"email": "user@example.com",
|
||||
"email_verified": true,
|
||||
"amr": ["mfa"],
|
||||
"name": "Jane Smith",
|
||||
"phone_number": "+15553805188",
|
||||
"phone_number_verified": false,
|
||||
"resource": "https://api.example.com",
|
||||
"agent_platform": "cursor",
|
||||
"agent_context_id": "chat-abc123"
|
||||
}
|
||||
```
|
||||
|
||||
### Required Claims
|
||||
|
||||
| Claim | Description |
|
||||
|-------|-------------|
|
||||
| `iss` | Provider's issuer URL (must be on service's trust list) |
|
||||
| `sub` | Opaque user identifier at the provider |
|
||||
| `aud` | Service's `resource` URL from the PRM |
|
||||
| `client_id` | Provider identity (issuer URL or CIMD URL) |
|
||||
| `jti` | Unique token ID for replay protection |
|
||||
| `iat` | Issuance time (epoch seconds) |
|
||||
| `exp` | Expiration (typically iat + 5 minutes) |
|
||||
| `auth_time` | Epoch seconds when the user last authenticated at the provider. **Required.** Service rejects ID-JAGs whose auth_time is older than its `idJagMaxAuthAgeSeconds` window. |
|
||||
| `email` + `email_verified: true` OR `phone_number` + `phone_number_verified: true` | At least one verified contact required |
|
||||
|
||||
### Optional Claims
|
||||
|
||||
| Claim | Description |
|
||||
|-------|-------------|
|
||||
| `amr` | Authentication methods reference (e.g., `["mfa"]`) |
|
||||
| `name` | User's display name |
|
||||
| `phone_number` | User's phone number |
|
||||
| `phone_number_verified` | Whether phone is verified |
|
||||
| `resource` | Resource server URL (informational) |
|
||||
| `agent_platform` | Agent platform (e.g., `"cursor"`, `"chatgpt"`) |
|
||||
| `agent_context_id` | Agent context/chat ID |
|
||||
|
||||
---
|
||||
|
||||
## Client ID Metadata Document (CIMD)
|
||||
|
||||
Optional document that decouples provider identity from signing keys. Defined by [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/).
|
||||
|
||||
Hosted at the URL used as `client_id` in the ID-JAG.
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "https://api.agent-provider.com/agent-auth.json",
|
||||
"client_name": "Agent Provider",
|
||||
"logo_uri": "https://agent-provider.com/logo.png",
|
||||
"client_uri": "https://agent-provider.com",
|
||||
"tos_uri": "https://agent-provider.com/tos",
|
||||
"policy_uri": "https://agent-provider.com/privacy",
|
||||
"token_endpoint_auth_method": "private_key_jwt",
|
||||
"jwks_uri": "https://agent-provider.com/.well-known/jwks.json",
|
||||
"scope": "openid email profile"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Identity Assertion JWT (Service-Signed)
|
||||
|
||||
After successful registration, the service returns an `identity_assertion` — a JWT signed by the service that the agent exchanges at `/oauth2/token` for an access_token.
|
||||
|
||||
The identity_assertion is:
|
||||
- Reusable until it expires (`assertion_expires`)
|
||||
- Exchanged via `POST /oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`
|
||||
- Replaced by a fresh one after claim ceremony completion (v2 carries user claims)
|
||||
|
||||
---
|
||||
|
||||
## Registration Response Shapes
|
||||
|
||||
### identity_assertion + id-jag (no confirmation needed)
|
||||
|
||||
```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "identity_assertion",
|
||||
"identity_assertion": "<service-signed-jwt>",
|
||||
"assertion_expires": "2026-05-04T13:00:00.000Z",
|
||||
"scopes": ["read", "write"]
|
||||
}
|
||||
```
|
||||
|
||||
### identity_assertion + id-jag (interaction_required — 401)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "interaction_required",
|
||||
"error_description": "...",
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "identity_assertion",
|
||||
"claim_url": "https://auth.example.com/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "...",
|
||||
"post_claim_scopes": ["read", "write"],
|
||||
"claim": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.example.com/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### service_auth
|
||||
|
||||
```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "service_auth",
|
||||
"claim_url": "https://auth.example.com/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "2026-05-21T17:31:25.994Z",
|
||||
"post_claim_scopes": ["read", "write"],
|
||||
"claim": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.example.com/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### anonymous
|
||||
|
||||
```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "anonymous",
|
||||
"identity_assertion": "<service-signed-jwt>",
|
||||
"assertion_expires": "2026-05-04T13:00:00.000Z",
|
||||
"pre_claim_scopes": ["read"],
|
||||
"claim_url": "https://auth.example.com/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "2026-05-21T17:26:32.915Z",
|
||||
"post_claim_scopes": ["read", "write"]
|
||||
}
|
||||
```
|
||||
|
||||
### Claim Ceremony Initiation (anonymous → POST /agent/identity/claim)
|
||||
|
||||
```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"claim_attempt_id": "cla_...",
|
||||
"status": "initiated",
|
||||
"expires_at": "2026-05-21T17:31:25.994Z",
|
||||
"claim_attempt": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "https://auth.example.com/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token Exchange Response (POST /oauth2/token — JWT-bearer grant)
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "<token>",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "read write"
|
||||
}
|
||||
```
|
||||
|
||||
### Claim Polling Success (POST /oauth2/token — claim grant)
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "<token>",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "read write",
|
||||
"identity_assertion": "<service-signed-jwt-v2>",
|
||||
"assertion_expires": "2026-05-21T18:31:25.994Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Response Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "<error_code>",
|
||||
"error_description": "<human-readable description>"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revocation
|
||||
|
||||
Two independent layers:
|
||||
|
||||
### Credential Layer (RFC 7009) — Agent-Callable
|
||||
|
||||
```http
|
||||
POST /oauth2/revoke
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
token=<access_token>&token_type_hint=access_token
|
||||
```
|
||||
|
||||
Kills one access_token. 200 on success, idempotent. The `identity_assertion` remains intact — re-run Step 5 for a fresh access_token.
|
||||
|
||||
### Registration Layer (RFC 8935 SET delivery) — Provider-Driven
|
||||
|
||||
Provider POSTs a Security Event Token (`Content-Type: application/secevent+jwt`) to the service's `events_endpoint`. The service invalidates the identity_assertion and all derived access_tokens.
|
||||
|
||||
Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3.
|
||||
|
||||
### SET Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://api.agent-provider.com",
|
||||
"sub": "<opaque-user-identifier>",
|
||||
"aud": "https://auth.example.com",
|
||||
"jti": "<unique-identifier>",
|
||||
"iat": 1716400000,
|
||||
"events": {
|
||||
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Processing
|
||||
|
||||
1. Verify signature against the issuer's JWKS (same trust path as ID-JAG verification)
|
||||
2. Enforce `jti` uniqueness for replay protection
|
||||
3. Find all identity_assertions and access_tokens for `(iss, sub, aud)` and invalidate them
|
||||
4. Return 200 on success, 400 on verification failure
|
||||
451
.github/skills/auth-md/references/protocol-template.md
vendored
Normal file
451
.github/skills/auth-md/references/protocol-template.md
vendored
Normal file
@@ -0,0 +1,451 @@
|
||||
# Protocol Template
|
||||
|
||||
Canonical template for generating an `auth.md` file (v2, June 2026). Replace all `{{placeholders}}` with service-specific values. Delete sections for flows the service does not support.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
An auth.md is organized as a numbered walkthrough the agent follows top to bottom:
|
||||
|
||||
1. **Title and intro** — addressed to the agent, declares real hostnames (resource server + auth server)
|
||||
2. **Step 1 — Discover** — two-hop discovery (PRM → AS metadata)
|
||||
3. **Step 2 — Pick a method** — decision tree
|
||||
4. **Step 3 — Register** — one subsection per supported method
|
||||
5. **Step 4 — Claim ceremony** — browser-based user_code ceremony (if service_auth or anonymous claim)
|
||||
6. **Step 5 — Exchange the assertion** — POST identity_assertion to /oauth2/token for access_token
|
||||
7. **Step 6 — Use the access_token** — how to use and refresh
|
||||
8. **Errors** — error codes table
|
||||
9. **Revocation** — two-layer revocation model
|
||||
|
||||
---
|
||||
|
||||
## Complete Template
|
||||
|
||||
```markdown
|
||||
# auth.md
|
||||
|
||||
You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API → handle revocation. Follow the steps in order; do not skip ahead.
|
||||
|
||||
Examples use placeholder hosts: `{{base_url}}` (the resource server hosting the API you want to call) and `{{auth_server_url}}` (the authorization server that handles registration).
|
||||
|
||||
## Step 1 — Discover
|
||||
|
||||
Discovery is two hops. The 401 response that pointed you here carries a `WWW-Authenticate` header with the PRM URL:
|
||||
|
||||
\```http
|
||||
HTTP/1.1 401 Unauthorized
|
||||
WWW-Authenticate: Bearer resource_metadata="{{base_url}}/.well-known/oauth-protected-resource"
|
||||
\```
|
||||
|
||||
### 1a. Fetch the Protected Resource Metadata
|
||||
|
||||
\```http
|
||||
GET /.well-known/oauth-protected-resource
|
||||
\```
|
||||
|
||||
Response:
|
||||
|
||||
\```json
|
||||
{
|
||||
"resource": "{{base_url}}/",
|
||||
"resource_name": "{{service_name}}",
|
||||
"resource_logo_uri": "{{logo_url}}",
|
||||
"authorization_servers": ["{{auth_server_url}}/"],
|
||||
"scopes_supported": [{{scopes_list}}],
|
||||
"bearer_methods_supported": ["header"]
|
||||
}
|
||||
\```
|
||||
|
||||
### 1b. Fetch the Authorization Server metadata
|
||||
|
||||
\```http
|
||||
GET /.well-known/oauth-authorization-server
|
||||
\```
|
||||
|
||||
Response:
|
||||
|
||||
\```json
|
||||
{
|
||||
"resource": "{{base_url}}/",
|
||||
"authorization_servers": ["{{auth_server_url}}/"],
|
||||
"scopes_supported": [{{scopes_list}}],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"issuer": "{{auth_server_url}}",
|
||||
"token_endpoint": "{{auth_server_url}}/oauth2/token",
|
||||
"revocation_endpoint": "{{auth_server_url}}/oauth2/revoke",
|
||||
"grant_types_supported": [
|
||||
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"urn:workos:agent-auth:grant-type:claim"
|
||||
],
|
||||
"agent_auth": {
|
||||
"skill": "{{auth_md_url}}",
|
||||
"identity_endpoint": "{{auth_server_url}}/agent/identity",
|
||||
"claim_endpoint": "{{auth_server_url}}/agent/identity/claim",
|
||||
"events_endpoint": "{{auth_server_url}}/agent/event/notify",
|
||||
"identity_types_supported": [{{identity_types}}],
|
||||
"identity_assertion": {
|
||||
"assertion_types_supported": [{{assertion_types}}]
|
||||
},
|
||||
"events_supported": [
|
||||
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
|
||||
]
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
## Step 2 — Pick a method
|
||||
|
||||
Use this decision tree:
|
||||
|
||||
1. **You have a session tied to a user identity and can exchange it for an ID-JAG, audience-bound to this service** → identity_assertion + id-jag.
|
||||
2. **You have only the user's email** → service_auth. Claim ceremony required.
|
||||
3. **You have neither** → anonymous. Claim ceremony optional; deferred until the user wants to take ownership.
|
||||
|
||||
Before sending: cross-check your choice against the `agent_auth` block. If your type is not in `identity_types_supported`, pick another or stop.
|
||||
|
||||
## Step 3 — Register
|
||||
|
||||
Before sending an `identity_assertion` or `service_auth` body, surface the service's `resource_name` and `resource_logo_uri` (from Step 1a) and the scope set you'll be acting under, and confirm with the user. Skip this for `anonymous`.
|
||||
|
||||
### identity_assertion + id-jag
|
||||
|
||||
<!-- DELETE THIS SECTION IF NOT SUPPORTING ID-JAG FLOW -->
|
||||
|
||||
Mint the ID-JAG with:
|
||||
- `aud` = the `resource` from the PRM
|
||||
- `iss` = your provider's issuer URL (must be on trust list)
|
||||
- `email_verified: true` OR `phone_number_verified: true`
|
||||
- Fresh `jti`, near-term `exp` (~5 minutes)
|
||||
- `auth_time` — epoch seconds when the user last authenticated at your provider. **Required.**
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "identity_assertion",
|
||||
"assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
|
||||
"assertion": "<ID-JAG>"
|
||||
}
|
||||
\```
|
||||
|
||||
Response — no confirmation needed (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "identity_assertion",
|
||||
"identity_assertion": "<service-signed-jwt>",
|
||||
"assertion_expires": "{{assertion_expiry}}",
|
||||
"scopes": [{{post_registration_scopes}}]
|
||||
}
|
||||
\```
|
||||
|
||||
Keep `identity_assertion` and go to Step 5.
|
||||
|
||||
Response — confirmation required (401, `interaction_required`):
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "interaction_required",
|
||||
"error_description": "...",
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "identity_assertion",
|
||||
"claim_url": "{{auth_server_url}}/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "...",
|
||||
"post_claim_scopes": [{{post_claim_scopes}}],
|
||||
"claim": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "{{auth_server_url}}/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
Surface `verification_uri` + `user_code` to the user (Step 4b) and poll (Step 4c).
|
||||
|
||||
Response — login required (401, `login_required`):
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "login_required",
|
||||
"error_description": "auth_time is too old; re-authenticate at the provider.",
|
||||
"max_age": 3600
|
||||
}
|
||||
\```
|
||||
|
||||
Re-authenticate the user at your provider (`prompt=login`) and mint a fresh ID-JAG.
|
||||
|
||||
### service_auth
|
||||
|
||||
<!-- DELETE THIS SECTION IF NOT SUPPORTING SERVICE_AUTH FLOW -->
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "service_auth",
|
||||
"login_hint": "user@example.com"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "service_auth",
|
||||
"claim_url": "{{auth_server_url}}/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "{{claim_ttl}}",
|
||||
"post_claim_scopes": [{{post_claim_scopes}}],
|
||||
"claim": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "{{auth_server_url}}/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
No `identity_assertion` yet. Go to Step 4.
|
||||
|
||||
### anonymous
|
||||
|
||||
<!-- DELETE THIS SECTION IF NOT SUPPORTING ANONYMOUS FLOW -->
|
||||
|
||||
\```http
|
||||
POST /agent/identity
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "anonymous"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"registration_type": "anonymous",
|
||||
"identity_assertion": "<service-signed-jwt>",
|
||||
"assertion_expires": "{{assertion_expiry}}",
|
||||
"pre_claim_scopes": [{{pre_claim_scopes}}],
|
||||
"claim_url": "{{auth_server_url}}/agent/identity/claim",
|
||||
"claim_token": "clm_...",
|
||||
"claim_token_expires": "{{claim_ttl}}",
|
||||
"post_claim_scopes": [{{post_claim_scopes}}]
|
||||
}
|
||||
\```
|
||||
|
||||
The `identity_assertion` exchanges at `/oauth2/token` for an access_token with `pre_claim_scopes` immediately (Step 5). To upgrade scopes, go to Step 4.
|
||||
|
||||
## Step 4 — Claim ceremony
|
||||
|
||||
<!-- DELETE THIS ENTIRE SECTION IF ONLY SUPPORTING identity_assertion WITHOUT interaction_required -->
|
||||
|
||||
The end goal: get a signed-in user to confirm a 6-digit `user_code` **you supply them**. The code travels from you → user; the user authenticates to the service and types it into a page the service owns.
|
||||
|
||||
### 4a. Get the ceremony materials
|
||||
|
||||
For **service_auth** registrations and **interaction_required** responses, you already have the `claim` block from Step 3. Skip to 4b.
|
||||
|
||||
For **anonymous** registrations, initiate the ceremony:
|
||||
|
||||
\```http
|
||||
POST /agent/identity/claim
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"claim_token": "clm_...",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"registration_id": "reg_...",
|
||||
"claim_attempt_id": "cla_...",
|
||||
"status": "initiated",
|
||||
"expires_at": "{{claim_attempt_ttl}}",
|
||||
"claim_attempt": {
|
||||
"user_code": "123456",
|
||||
"expires_in": 600,
|
||||
"verification_uri": "{{auth_server_url}}/login?return_to=...",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
\```
|
||||
|
||||
### 4b. Hand off to the user
|
||||
|
||||
Surface `verification_uri` and `user_code` to the user in a single message:
|
||||
|
||||
> Open this link, sign in (or sign up), and enter this 6-digit code: **123456**
|
||||
> {{verification_uri}}
|
||||
|
||||
The user will:
|
||||
1. Open `verification_uri`
|
||||
2. Authenticate with the service (sign in or sign up)
|
||||
3. Land on the claim page, see their identity displayed, type the `user_code`, and submit
|
||||
|
||||
### 4c. Poll for completion
|
||||
|
||||
Poll the standard `token_endpoint` (from AS metadata) with the profile-specific claim grant:
|
||||
|
||||
\```http
|
||||
POST /oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:workos:agent-auth:grant-type:claim
|
||||
&claim_token=<claim_token>
|
||||
\```
|
||||
|
||||
Response while waiting:
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "authorization_pending",
|
||||
"error_description": "..."
|
||||
}
|
||||
\```
|
||||
|
||||
Response on success:
|
||||
|
||||
\```json
|
||||
{
|
||||
"access_token": "<token>",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "{{scopes}}",
|
||||
"identity_assertion": "<service-signed-jwt-v2>",
|
||||
"assertion_expires": "{{assertion_expiry}}"
|
||||
}
|
||||
\```
|
||||
|
||||
Use `access_token` immediately; cache `identity_assertion` for refresh via Step 5.
|
||||
|
||||
If the `user_code` window expires:
|
||||
|
||||
\```json
|
||||
{
|
||||
"error": "expired_token",
|
||||
"error_description": "..."
|
||||
}
|
||||
\```
|
||||
|
||||
Re-call `POST /agent/identity/claim` with the same `claim_token` and `email` to mint a fresh `user_code`. If that returns `claim_expired`, restart at Step 3.
|
||||
|
||||
Honor `interval` (in seconds); on `slow_down` back off.
|
||||
|
||||
## Step 5 — Exchange the assertion
|
||||
|
||||
POST the `identity_assertion` to the token endpoint with the RFC 7523 JWT-bearer grant:
|
||||
|
||||
\```http
|
||||
POST /oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
&assertion=<identity_assertion>
|
||||
&resource={{base_url}}/
|
||||
\```
|
||||
|
||||
Response (200):
|
||||
|
||||
\```json
|
||||
{
|
||||
"access_token": "<token>",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "{{scopes}}"
|
||||
}
|
||||
\```
|
||||
|
||||
The same `identity_assertion` can be re-used to mint additional access_tokens until it expires. If `/oauth2/token` returns `invalid_grant`, restart at Step 3.
|
||||
|
||||
## Step 6 — Use the access_token
|
||||
|
||||
Present as a bearer token:
|
||||
|
||||
\```http
|
||||
GET /api/some-resource
|
||||
Authorization: Bearer <access_token>
|
||||
\```
|
||||
|
||||
**Refresh:** When the access_token expires, re-call Step 5 with the same `identity_assertion`. When the identity_assertion itself expires or `/oauth2/token` returns `invalid_grant`, restart at Step 3. There is no refresh_token — the two-step pattern replaces it.
|
||||
|
||||
On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1.
|
||||
|
||||
Full API reference: `{{api_docs_url}}`
|
||||
|
||||
## Errors
|
||||
|
||||
| Code | Where | What to do |
|
||||
|------|-------|------------|
|
||||
| `anonymous_not_enabled` | `/agent/identity` | Pick another method from Step 2 |
|
||||
| `service_auth_not_enabled` | `/agent/identity` | Pick another method |
|
||||
| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list. Pick another method |
|
||||
| `invalid_request` | `/agent/identity` | Fix body shape, claims, signature, jti, aud problems |
|
||||
| `interaction_required` (401) | `/agent/identity` (ID-JAG) | Body carries `claim` block; surface to user (Step 4) |
|
||||
| `login_required` (401) | `/agent/identity` (ID-JAG) | Re-authenticate user at provider, mint fresh ID-JAG |
|
||||
| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3 |
|
||||
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed. Re-read Step 3 response |
|
||||
| `claim_expired` | `/agent/identity/claim` | Registration expired. Restart at Step 3 |
|
||||
| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 |
|
||||
| `invalid_client` | `/oauth2/token` | client_id not recognized |
|
||||
| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grant types |
|
||||
| `authorization_pending` | `/oauth2/token` (claim) | User hasn't completed ceremony. Honor `interval` |
|
||||
| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart |
|
||||
| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval and retry |
|
||||
| `rate_limited` (429) | any | Back off and retry |
|
||||
|
||||
## Revocation
|
||||
|
||||
Two independent layers:
|
||||
|
||||
- **Credential layer (RFC 7009):** POST `token=<access_token>&token_type_hint=access_token` to `{{auth_server_url}}/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5.
|
||||
- **Registration layer (RFC 8935 SET delivery):** Provider POSTs a Security Event Token to `events_endpoint`. Service invalidates identity_assertion and all derived access_tokens. Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3.
|
||||
|
||||
On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation — fresh access_token works. If `invalid_grant`, registration-layer — restart at Step 3.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Reference
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| `{{base_url}}` | API base URL (resource server) | `https://api.acme.com` |
|
||||
| `{{service_name}}` | Human-readable service name | `Acme Notes` |
|
||||
| `{{logo_url}}` | Service logo URL | `https://acme.com/logo.png` |
|
||||
| `{{auth_server_url}}` | Authorization server base URL | `https://auth.acme.com` |
|
||||
| `{{auth_md_url}}` | URL where auth.md is hosted | `https://acme.com/auth.md` |
|
||||
| `{{scopes_list}}` | JSON array of scope strings | `"notes.read", "notes.write"` |
|
||||
| `{{identity_types}}` | Supported identity types | `"anonymous", "identity_assertion", "service_auth"` |
|
||||
| `{{assertion_types}}` | Supported assertion types | `"urn:ietf:params:oauth:token-type:id-jag"` |
|
||||
| `{{pre_claim_scopes}}` | Scopes before claim (anonymous) | `"notes.read"` |
|
||||
| `{{post_claim_scopes}}` | Scopes after claim | `"notes.read", "notes.write"` |
|
||||
| `{{post_registration_scopes}}` | Scopes after ID-JAG registration | `"notes.read", "notes.write"` |
|
||||
| `{{assertion_expiry}}` | Identity assertion expiry (ISO) | `2026-05-22T13:00:00.000Z` |
|
||||
| `{{claim_ttl}}` | Claim token expiration (ISO) | `2026-05-22T12:00:00.000Z` |
|
||||
| `{{claim_attempt_ttl}}` | Claim attempt expiration | `2026-05-22T12:10:00.000Z` |
|
||||
| `{{api_docs_url}}` | Link to full API docs | `https://docs.acme.com/` |
|
||||
|
||||
---
|
||||
|
||||
## Generation Rules
|
||||
|
||||
1. **Keep the file concise and high-signal** — anything the agent doesn't need to register or operate belongs in main documentation, not in auth.md
|
||||
2. **Use fenced code blocks with language hints** (`http`, `json`) so agents can extract templates unambiguously
|
||||
3. **Declare real hostnames in the intro** — resource server and auth server — so the agent knows which host each example targets
|
||||
4. **Delete sections for unsupported flows** — don't leave empty sections or "N/A" markers
|
||||
5. **The PRM is authoritative** — if anything in auth.md conflicts with the PRM, the PRM wins
|
||||
6. **Token exchange is always required** — registration never returns an access_token directly; always returns an identity_assertion that must be exchanged at `/oauth2/token`
|
||||
162
.github/skills/auth-md/references/validation-rules.md
vendored
Normal file
162
.github/skills/auth-md/references/validation-rules.md
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
# Validation Rules
|
||||
|
||||
Complete ruleset for validating `auth.md` files against the protocol specification (v2, June 2026).
|
||||
|
||||
---
|
||||
|
||||
## Level: Basic (Offline)
|
||||
|
||||
### Structure Rules
|
||||
|
||||
| ID | Rule | Error Message | Severity |
|
||||
|----|------|---------------|----------|
|
||||
| S01 | Document starts with `# auth.md` heading | Missing required top-level heading `# auth.md` | 🔴 |
|
||||
| S02 | Contains `## Step 1 — Discover` section | Missing required section: Step 1 — Discover | 🔴 |
|
||||
| S03 | Contains `## Step 2 — Pick a method` section | Missing required section: Step 2 — Pick a method | 🔴 |
|
||||
| S04 | Contains `## Step 3 — Register` section | Missing required section: Step 3 — Register | 🔴 |
|
||||
| S05 | Contains `## Step 4 — Claim ceremony` section (if service_auth or anonymous with claim) | Missing required section: Step 4 — Claim ceremony (required when service_auth or anonymous claim is supported) | 🔴 |
|
||||
| S06 | Contains `## Step 5 — Exchange the assertion` section | Missing required section: Step 5 — Exchange the assertion | 🔴 |
|
||||
| S07 | Contains `## Step 6 — Use the access_token` section | Missing required section: Step 6 — Use the access_token | 🔴 |
|
||||
| S08 | Contains `## Errors` section | Missing required section: Errors | 🔴 |
|
||||
| S09 | Contains `## Revocation` section | Missing required section: Revocation | 🔴 |
|
||||
| S10 | Sections appear in correct order (Step 1 → 2 → 3 → 4 → 5 → 6 → Errors → Revocation) | Sections are out of order. Expected sequence: Step 1, 2, 3, 4, 5, 6, Errors, Revocation | 🟡 |
|
||||
| S11 | Intro declares real hostnames (resource server and auth server) | Intro should declare real hostnames for resource and auth servers | 🟢 |
|
||||
|
||||
### Field Rules
|
||||
|
||||
| ID | Rule | Error Message | Severity |
|
||||
|----|------|---------------|----------|
|
||||
| F01 | Step 1 contains at least one fenced JSON block with `resource` field | Step 1 must include Protected Resource Metadata JSON with `resource` field | 🔴 |
|
||||
| F02 | PRM JSON contains `resource_name` | Missing `resource_name` in PRM — agents need this for consent prompts | 🟡 |
|
||||
| F03 | PRM JSON contains `resource_logo_uri` | Missing `resource_logo_uri` — recommended for consent UX | 🟢 |
|
||||
| F04 | JSON metadata contains `authorization_servers` array | Missing `authorization_servers` — agents can't discover the auth endpoint | 🟡 |
|
||||
| F05 | JSON metadata contains `scopes_supported` array with ≥1 scope | Missing or empty `scopes_supported` | 🟡 |
|
||||
| F06 | AS metadata contains `agent_auth` block | Missing `agent_auth` block in Authorization Server metadata | 🔴 |
|
||||
| F07 | `agent_auth` contains `identity_endpoint` | Missing `identity_endpoint` in agent_auth block | 🔴 |
|
||||
| F08 | `agent_auth` contains `identity_types_supported` with ≥1 type | Missing or empty `identity_types_supported` | 🔴 |
|
||||
| F09 | If `identity_assertion` in identity_types_supported, `assertion_types_supported` must exist | Declared `identity_assertion` support but missing `assertion_types_supported` | 🟡 |
|
||||
| F10 | Step 3 contains at least one `POST /agent/identity` request example | Step 3 must include at least one registration request example | 🔴 |
|
||||
| F11 | Errors section contains a table with `Code`, `Where`, and `What to do` columns | Errors section must contain a table with Code, Where, and What to do columns | 🟡 |
|
||||
| F12 | `agent_auth` contains `skill` pointing to auth.md URL | Missing `skill` field in agent_auth — recommended for discoverability | 🟢 |
|
||||
| F13 | `agent_auth` contains `events_supported` | Missing `events_supported` — recommended for revocation support | 🟢 |
|
||||
| F14 | AS metadata contains `token_endpoint` | Missing `token_endpoint` — required for token exchange and claim polling | 🔴 |
|
||||
| F15 | AS metadata contains `revocation_endpoint` | Missing `revocation_endpoint` — required for credential-layer revocation | 🟡 |
|
||||
| F16 | AS metadata contains `grant_types_supported` with both required URNs | Missing or incomplete `grant_types_supported` — must include jwt-bearer and claim grant URNs | 🟡 |
|
||||
| F17 | If `service_auth` or `anonymous` in identity_types_supported, `claim_endpoint` must exist | service_auth/anonymous requires `claim_endpoint` for ceremony initiation | 🟡 |
|
||||
| F18 | `agent_auth` contains `events_endpoint` | Missing `events_endpoint` — recommended for registration-layer revocation | 🟢 |
|
||||
| F19 | Step 5 contains `POST /oauth2/token` with jwt-bearer grant | Step 5 must document the token exchange via /oauth2/token | 🔴 |
|
||||
|
||||
### Consistency Rules
|
||||
|
||||
| ID | Rule | Error Message | Severity |
|
||||
|----|------|---------------|----------|
|
||||
| C01 | Flows documented in Step 3 match `identity_types_supported` in metadata | Mismatch: Step 3 documents flows not declared in metadata (or vice versa) | 🟡 |
|
||||
| C02 | If only identity_assertion flow without claim: Step 4 may be absent | Step 4 present but only identity_assertion flow without claim is supported | 🟡 |
|
||||
| C03 | `identity_endpoint` path matches the POST path in Step 3 examples | Registration endpoint in metadata doesn't match the POST path in Step 3 | 🟡 |
|
||||
| C04 | `claim_endpoint` path matches the POST path in Step 4 examples (if present) | Claim endpoint in metadata doesn't match the POST path in Step 4 | 🟡 |
|
||||
| C05 | Scopes in response examples are subset of `scopes_supported` | Response example contains scopes not listed in `scopes_supported` | 🟡 |
|
||||
| C06 | `resource` URL is consistent across all JSON blocks | Different `resource` URLs found in metadata blocks — must be consistent | 🟡 |
|
||||
| C07 | All URLs use HTTPS scheme | Non-HTTPS URL found — all endpoints must use HTTPS | 🟡 |
|
||||
| C08 | Error codes in table match standard protocol error codes (see list below) | Non-standard error code found | 🟡 |
|
||||
| C09 | `aud` in ID-JAG examples matches the `resource` from PRM | ID-JAG `aud` example doesn't match the resource URL | 🟡 |
|
||||
| C10 | `assertion_types_supported` includes types used in Step 3 examples | Step 3 uses assertion types not declared in metadata | 🟡 |
|
||||
| C11 | `token_endpoint` in metadata matches the `/oauth2/token` path used in Steps 4c and 5 | Token endpoint in metadata doesn't match the POST path in Steps 4c/5 | 🟡 |
|
||||
| C12 | `grant_types_supported` includes `urn:ietf:params:oauth:grant-type:jwt-bearer` | Missing jwt-bearer grant in grant_types_supported — required for token exchange | 🟡 |
|
||||
| C13 | `grant_types_supported` includes `urn:workos:agent-auth:grant-type:claim` (if claim ceremony exists) | Missing claim grant in grant_types_supported — required for ceremony polling | 🟡 |
|
||||
|
||||
### Format Rules
|
||||
|
||||
| ID | Rule | Error Message | Severity |
|
||||
|----|------|---------------|----------|
|
||||
| X01 | All JSON in fenced code blocks is valid JSON | Invalid JSON in fenced code block at section: {section} | 🟡 |
|
||||
| X02 | HTTP request examples use valid HTTP method + path | Invalid HTTP request format. Expected: METHOD /path | 🟡 |
|
||||
| X03 | No unreplaced placeholder patterns (`{{...}}`, `<your-...>`, `[YOUR_...]`) | Unreplaced placeholder found: {placeholder} | 🟡 |
|
||||
| X04 | Fenced code blocks have language hint (`http`, `json`) | Code block missing language hint — agents use these to identify request shapes | 🟢 |
|
||||
|
||||
---
|
||||
|
||||
## Level: Full (Live)
|
||||
|
||||
All Basic rules, plus:
|
||||
|
||||
### Endpoint Rules
|
||||
|
||||
| ID | Rule | Error Message | Severity |
|
||||
|----|------|---------------|----------|
|
||||
| E01 | `GET {base_url}/.well-known/oauth-protected-resource` returns 200 with valid JSON | Protected Resource Metadata endpoint not reachable or returns invalid response | 🔴 |
|
||||
| E02 | PRM response contains `authorization_servers` pointing to AS with `agent_auth` block | No `agent_auth` block discoverable through PRM → AS metadata chain | 🔴 |
|
||||
| E03 | `GET {auth_server}/.well-known/oauth-authorization-server` returns 200 with valid JSON | Authorization Server metadata endpoint not reachable | 🔴 |
|
||||
| E04 | AS metadata `agent_auth` block matches declarations in auth.md | Live AS metadata `agent_auth` block differs from auth.md declarations | 🟡 |
|
||||
| E05 | `identity_endpoint` accepts POST (returns 400/401/422, not 404/405) | Registration endpoint returns 404 or 405 — not implemented | 🔴 |
|
||||
| E06 | `claim_endpoint` accepts POST (if service_auth or anonymous supported) | Claim endpoint returns 404 or 405 — not implemented | 🔴 |
|
||||
| E07 | `token_endpoint` accepts POST (returns 400/401, not 404/405) | Token endpoint returns 404 or 405 — not implemented | 🔴 |
|
||||
| E08 | `revocation_endpoint` accepts POST (returns 200/400, not 404/405) | Revocation endpoint returns 404 or 405 — not implemented | 🟡 |
|
||||
| E09 | API base URL returns 401 with `WWW-Authenticate` header containing `resource_metadata` | API does not return WWW-Authenticate header with resource_metadata on 401 | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
## Standard Protocol Error Codes
|
||||
|
||||
Complete list of error codes the Errors table should cover (per supported flows):
|
||||
|
||||
### Registration Endpoint (`/agent/identity`)
|
||||
- `anonymous_not_enabled`
|
||||
- `service_auth_not_enabled`
|
||||
- `issuer_not_enabled`
|
||||
- `invalid_request` (body shape, missing claims, signature, jti, aud, unverified identity)
|
||||
- `interaction_required` (401) — ID-JAG matched account but no (iss,sub) delegation
|
||||
- `login_required` (401) — auth_time missing or older than max_age
|
||||
- `rate_limited` (429)
|
||||
|
||||
### Claim Endpoint (`/agent/identity/claim`)
|
||||
- `invalid_claim_token`
|
||||
- `claimed_or_in_flight`
|
||||
- `claim_expired`
|
||||
|
||||
### Token Endpoint (`/oauth2/token`)
|
||||
- `invalid_grant` — assertion expired/revoked/replayed
|
||||
- `invalid_client`
|
||||
- `unsupported_grant_type`
|
||||
- `authorization_pending` — claim polling, user hasn't completed
|
||||
- `expired_token` — user_code window closed
|
||||
- `slow_down` — polling too fast
|
||||
|
||||
### All Endpoints
|
||||
- `rate_limited` (429)
|
||||
|
||||
---
|
||||
|
||||
## Validation Report Format
|
||||
|
||||
```markdown
|
||||
# Validation Report — auth.md
|
||||
|
||||
**File:** {path_or_url}
|
||||
**Level:** {basic|full}
|
||||
**Date:** {timestamp}
|
||||
|
||||
## Summary
|
||||
|
||||
- ✅ {n} rules passed
|
||||
- ❌ {n} rules failed
|
||||
- ⚠️ {n} warnings
|
||||
|
||||
## Structure
|
||||
|
||||
| Status | ID | Rule |
|
||||
|--------|-----|-------|
|
||||
| ✅ | S01 | Heading `# auth.md` present |
|
||||
| ❌ | S06 | Step 5 section missing |
|
||||
|
||||
## Fields
|
||||
...
|
||||
|
||||
## Consistency
|
||||
...
|
||||
|
||||
## Format
|
||||
...
|
||||
|
||||
## Endpoints (full only)
|
||||
...
|
||||
```
|
||||
Reference in New Issue
Block a user