Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions plugins/auth0/skills/auth0-custom-token-exchange/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
name: auth0-custom-token-exchange
description: Use when implementing Custom Token Exchange (RFC 8693) with nextjs-auth0 — exchanging external tokens (legacy system tokens, partner SSO tokens, third-party IdP tokens) for Auth0 access tokens server-side. Covers auth0.customTokenExchange(), CustomTokenExchangeError handling, creating token exchange profiles via CLI or Terraform, and the tenant configuration required for the exchange to succeed.
license: Apache-2.0
metadata:
author: Auth0 <support@auth0.com>
version: '1.0.0'
openclaw:
emoji: "🔐"
homepage: https://github.com/auth0/agent-skills
requires:
bins:
- auth0
os:
- darwin
- linux
install:
- id: brew
kind: brew
package: auth0/auth0-cli/auth0
bins: [auth0]
label: 'Install Auth0 CLI (brew)'
---

# Auth0 Custom Token Exchange

Exchange external tokens for Auth0 access tokens using RFC 8693. Works for legacy system migration, third-party federation, and agent delegation flows.

## What this does

`auth0.customTokenExchange()` calls Auth0's token endpoint server-side, presenting an external token (your `subjectToken`) and receiving an Auth0 access token in return. Auth0 validates the exchange via an Action you deploy, then issues the token scoped to your API audience.

**Critical constraint**: server-side only. The call requires `AUTH0_CLIENT_SECRET` — it cannot run in a Client Component.

**No session is created.** The exchange returns tokens only. `auth0.getSession()` will still return `null` after this call.
Comment on lines +25 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add the missing prerequisites and "when not to use" sections.

The skill spec calls for both, but this doc jumps straight into usage. Please add a short prerequisites block and a brief "when not to use" note near the top. As per coding guidelines, SKILL.md should document prerequisites and when NOT to use guidance.

🧰 Tools
🪛 SkillSpector (2.1.1)

[error] 3: [PE3] Credential Access: Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Remediation: Remove references to credential paths. Use environment variables or secrets managers. For docs, use placeholder paths (e.g., /path/to/config). Never load .env or token files in production code paths.

(Privilege Escalation (PE3))


[error] 27: [PE3] Credential Access: Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Remediation: Remove references to credential paths. Use environment variables or secrets managers. For docs, use placeholder paths (e.g., /path/to/config). Never load .env or token files in production code paths.

(Privilege Escalation (PE3))


[error] 31: [PE3] Credential Access: Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Remediation: Remove references to credential paths. Use environment variables or secrets managers. For docs, use placeholder paths (e.g., /path/to/config). Never load .env or token files in production code paths.

(Privilege Escalation (PE3))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/auth0/skills/auth0-custom-token-exchange/SKILL.md` around lines 25 -
35, The SKILL.md file for auth0-custom-token-exchange is missing two required
sections according to the skill spec guidelines. Add a Prerequisites section
that lists what is needed to use auth0.customTokenExchange() (such as Auth0
configuration, required environment variables, and setup steps), and add a When
Not to Use section that briefly describes scenarios where this skill should not
be used. Insert both sections near the top of the document, after the opening
description but before the "What this does" section, to provide users with
critical context before they attempt to use the skill.

Source: Coding guidelines


## SDK pattern — Next.js (nextjs-auth0 v4)

```ts
// app/api/exchange-token/route.ts ← Server-side: Route Handler, Server Component, or Server Action only
import { auth0 } from '@/lib/auth0';
import { CustomTokenExchangeError, CustomTokenExchangeErrorCode } from '@auth0/nextjs-auth0/errors';

export async function POST(request: Request) {
const { legacyToken } = await request.json();

try {
const result = await auth0.customTokenExchange({
subjectToken: legacyToken,
subjectTokenType: 'urn:acme:legacy-token', // must match profile config exactly
audience: 'https://api.example.com',
});

return Response.json({ accessToken: result.accessToken });
} catch (error) {
if (error instanceof CustomTokenExchangeError) {
switch (error.code) {
case CustomTokenExchangeErrorCode.MISSING_SUBJECT_TOKEN:
case CustomTokenExchangeErrorCode.INVALID_SUBJECT_TOKEN_TYPE:
return Response.json({ error: 'Invalid token' }, { status: 400 });
case CustomTokenExchangeErrorCode.EXCHANGE_FAILED:
return Response.json({ error: 'Exchange rejected by server' }, { status: 401 });
default:
return Response.json({ error: error.message }, { status: 400 });
}
}
throw error;
}
}
```

### Delegation flow (acting on behalf of a user)

```ts
const result = await auth0.customTokenExchange({
subjectToken: userToken,
subjectTokenType: 'urn:acme:user-token',
actorToken: agentToken,
actorTokenType: 'https://idp.example.com/token-type/agent',
audience: 'https://downstream-api.example.com',
});

// Decoded act claim — identifies the acting party
console.log(result.act); // { sub: "agent|abc123" }

// result.refreshToken is UNDEFINED in delegation flows — expected, not a bug
```

## Tenant configuration required

### Step 1 — Create and deploy a Custom Token Exchange Action

The Action validates the incoming `subject_token`. Auth0 calls it before issuing the exchange token.

```bash
# Create the action
auth0 api POST /api/v2/actions \
--data '{
"name": "cte-validator",
"supported_triggers": [{"id": "custom-token-exchange", "version": "v1"}],
"code": "exports.onExecuteCustomTokenExchange = async (event, api) => { const token = event.request.body.subject_token; if (!token || !isValidLegacyToken(token)) { api.access.deny(\"invalid_subject_token\", \"Token validation failed\"); return; } await api.authentication.setUserByConnection({ connection: \"Username-Password-Authentication\", user_id: \"auth0|\" + getUserIdFromToken(token) }); };"
}'

# Deploy it (use the id returned above)
auth0 api POST /api/v2/actions/{id}/deploy
```

### Step 2 — Create the token exchange profile

```bash
auth0 api POST /api/v2/token-exchange-profiles \
--data '{
"name": "legacy-migration",
"subject_token_type": "urn:acme:legacy-token",
"action_id": "<deployed-action-id>",
"type": "custom_authentication"
}'
```

### Terraform

```hcl
resource "auth0_action" "cte_validator" {
name = "cte-validator"
code = "exports.onExecuteCustomTokenExchange = async (event, api) => { /* validate and setUserByConnection */ };"
deploy = true
supported_triggers {
id = "custom-token-exchange"
version = "v1"
}
}

resource "auth0_token_exchange_profile" "legacy" {
name = "legacy-migration"
subject_token_type = "urn:acme:legacy-token" # must match subjectTokenType in code exactly
action_id = auth0_action.cte_validator.id
type = "custom_authentication"
}
```

## Common failure modes

| Failure | Cause | How it surfaces |
|---------|-------|-----------------|
| `EXCHANGE_FAILED` with no clear cause | `subjectTokenType` in code doesn't match `subject_token_type` in profile | No mismatch hint — check exact string match |
| `EXCHANGE_FAILED` — no matching profile | Token exchange profile was never created | CLI/Terraform config step was skipped |
| `EXCHANGE_FAILED` — action error | Action not deployed, or Action calls `api.access.deny()` | Check action deployment status; check `fecte` logs |
| `result.refreshToken` is `undefined` | Delegation flow (`actorToken` present) — Auth0 suppresses refresh tokens intentionally | Expected — do not treat as a bug |
| `null` session after exchange | `customTokenExchange` never creates a session | `getSession()` returns `null` — tokens are returned directly, not stored |
| Client-side failure | `customTokenExchange()` called in a Client Component | Requires `client_secret` — server-side only |

## `subjectTokenType` rules

- Must be a valid URI: starts with `urn:`, `https://`, or `http://`
- Length: 10–100 characters
- Avoid reserved namespaces: `urn:ietf:` and `urn:auth0:` are rejected
- Must match the `subject_token_type` in the token exchange profile **exactly** (string equality)

## Checklist

- [ ] `auth0.customTokenExchange()` called in a Server Component, Route Handler, or Server Action
- [ ] `CustomTokenExchangeError` imported from `@auth0/nextjs-auth0/errors` and handled explicitly
- [ ] `subjectTokenType` is a valid URI (not `urn:ietf:` or `urn:auth0:`)
- [ ] Auth0 Action with `custom-token-exchange` trigger created and deployed
- [ ] Token exchange profile created with `subject_token_type` matching code exactly
- [ ] Not expecting a session after exchange (`getSession()` returns `null`)
- [ ] Delegation flows: not expecting `result.refreshToken` when `actorToken` is passed
Loading