-
Notifications
You must be signed in to change notification settings - Fork 23
feat(skills): add auth0-custom-token-exchange skill #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brth31
wants to merge
1
commit into
main
Choose a base branch
from
feat/add-auth0-custom-token-exchange-skill
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
plugins/auth0/skills/auth0-custom-token-exchange/SKILL.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| ## 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
Source: Coding guidelines