Auth0 Integration
Integrate Auth0 as the identity provider for your marketplace application. This guide uses the Management API v2 for user provisioning and the Authentication API for token acquisition.
Prerequisites
- Auth0 Tenant: Developer or production tenant
- Regular Web Application with:
- Client ID and Client Secret
- "Password" grant enabled (Application > Advanced > Grant Types)
- Appropriate API audience configured
- Database Connection:
Username-Password-Authentication(default) or custom - Roles: Create a
gw-userrole under User Management > Roles - Login Action: Deploy an Action to inject custom claims into tokens
Auth0 Tenant Setup
-
Create a Regular Web Application:
Application Type: Regular Web ApplicationName: GW Marketplace IntegrationGrant Types: Authorization Code, Client Credentials, Password -
Create a
gw-userRole:- User Management > Roles > Create Role
- Name:
gw-user
-
Deploy a Login Action (Login flow) to add custom claims to tokens:
exports.onExecutePostLogin = async (event, api) => {const namespace = 'https://gw-marketplace/';api.accessToken.setCustomClaim(namespace + 'roles', event.authorization?.roles || []);api.accessToken.setCustomClaim(namespace + 'orgId', event.user.app_metadata?.orgId || '');api.accessToken.setCustomClaim(namespace + 'gwUserId', event.user.app_metadata?.gwUserId || '');api.accessToken.setCustomClaim(namespace + 'gwApplicationId',event.user.app_metadata?.gwApplicationId || '');}; -
Grant Management API Permissions to your application:
- APIs > Auth0 Management API > Machine to Machine Applications
- Authorize your application with scopes:
read:users,create:users,update:users,read:roles,create:role_members
Environment Variables
# Auth0 Tenant
AUTH0_DOMAIN=your-tenant.us.auth0.com
# Application Credentials
AUTH0_CLIENT_ID=your-client-id
AUTH0_CLIENT_SECRET=your-client-secret
# Database Connection (default: Username-Password-Authentication)
AUTH0_CONNECTION=Username-Password-Authentication
# API Audience (optional -- defaults to Management API)
AUTH0_AUDIENCE=https://your-api.example.com
# Marketplace Configuration
MARKETPLACE_APP_ID=your-registered-app-id
Exchange Flow
sequenceDiagram
participant SDK as Provider SDK
participant Backend as Your Backend
participant Auth0 as Auth0
SDK->>Backend: POST /auth/exchange (Bearer marketplace-jwt)
Backend->>Backend: Validate JWT (signature, iss, applicationId, exp)
Backend->>Auth0: POST /oauth/token (client_credentials)
Auth0-->>Backend: Management API token
Backend->>Auth0: GET /api/v2/users?q=app_metadata.gwUserId:"..."
alt User exists
Auth0-->>Backend: User record
Backend->>Auth0: PATCH /api/v2/users/{id} (update app_metadata)
else User not found
Backend->>Auth0: POST /api/v2/users (create with password)
Auth0-->>Backend: Created user
Backend->>Auth0: POST /api/v2/users/{id}/roles (assign gw-user)
end
Backend->>Auth0: POST /oauth/token (password grant)
Auth0-->>Backend: access_token + refresh_token
Backend-->>SDK: Tokens + user info
Backend Implementation
import { createHash } from 'crypto';
const AUTH0_BASE_URL = `https://${process.env.AUTH0_DOMAIN}`;
const CLAIM_NAMESPACE = 'https://gw-marketplace/';
// Deterministic password derivation
function derivedPassword(userId: string): string {
const hash = createHash('sha256')
.update(`${userId}${process.env.AUTH0_CLIENT_SECRET}`)
.digest('hex');
// Auth0 requires: 8+ chars, 1 upper, 1 lower, 1 number, 1 special
return `Gw!${hash.slice(0, 29)}`;
}
// Obtain Management API token (cached until expiry)
let mgmtToken: string | null = null;
let mgmtTokenExpiry = 0;
async function getManagementToken(): Promise<string> {
if (mgmtToken && Date.now() < mgmtTokenExpiry - 10_000) {
return mgmtToken;
}
const res = await fetch(`${AUTH0_BASE_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
audience: `${AUTH0_BASE_URL}/api/v2/`,
}),
});
if (!res.ok) throw new Error(`Management token failed: ${res.status}`);
const data = await res.json();
mgmtToken = data.access_token;
mgmtTokenExpiry = Date.now() + data.expires_in * 1000;
return data.access_token;
}
// Search for existing user by gwUserId in app_metadata
async function findUser(gwUserId: string) {
const token = await getManagementToken();
const q = encodeURIComponent(`app_metadata.gwUserId:"${gwUserId}"`);
const res = await fetch(`${AUTH0_BASE_URL}/api/v2/users?q=${q}&search_engine=v3`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`User search failed: ${res.status}`);
const users = await res.json();
return users.length > 0 ? users[0] : null;
}
// Create user with password and app_metadata
async function createUser(claims: any) {
const token = await getManagementToken();
const email = claims.email ?? `${claims.userId}@gw-marketplace.local`;
const password = derivedPassword(claims.userId);
const res = await fetch(`${AUTH0_BASE_URL}/api/v2/users`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
connection: process.env.AUTH0_CONNECTION ?? 'Username-Password-Authentication',
email,
password,
email_verified: true,
app_metadata: {
gwUserId: claims.userId,
orgId: claims.orgId,
gwApplicationId: claims.applicationId,
},
}),
});
if (!res.ok) throw new Error(`Create user failed: ${res.status}`);
return res.json();
}
// Assign a role to a user
async function assignRole(userId: string, roleName: string) {
const token = await getManagementToken();
// Find role by name
const rolesRes = await fetch(
`${AUTH0_BASE_URL}/api/v2/roles?name_filter=${encodeURIComponent(roleName)}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
const roles = await rolesRes.json();
if (roles.length === 0) return;
// Assign role to user
await fetch(`${AUTH0_BASE_URL}/api/v2/users/${encodeURIComponent(userId)}/roles`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ roles: [roles[0].id] }),
});
}
// Obtain user tokens via Resource Owner Password grant
async function getUserTokens(email: string, password: string) {
const body: Record<string, string> = {
grant_type: 'password',
client_id: process.env.AUTH0_CLIENT_ID!,
client_secret: process.env.AUTH0_CLIENT_SECRET!,
username: email,
password,
scope: 'openid profile email offline_access',
};
// Set audience if configured for a custom API
if (process.env.AUTH0_AUDIENCE) {
body.audience = process.env.AUTH0_AUDIENCE;
}
const res = await fetch(`${AUTH0_BASE_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Token grant failed: ${res.status}`);
return res.json();
}
Token Structure
Auth0 access tokens contain custom claims under a namespace:
{
"iss": "https://your-tenant.us.auth0.com/",
"exp": 1714000000,
"sub": "auth0|abc123def456",
"email": "user@example.com",
"nickname": "gw-user-abc123",
"https://gw-marketplace/roles": ["gw-user"],
"https://gw-marketplace/orgId": "org-456",
"https://gw-marketplace/gwUserId": "user-abc123",
"https://gw-marketplace/gwApplicationId": "app-789"
}
Access custom claims using the namespace prefix:
function decodeAccessToken(accessToken: string) {
const payload = JSON.parse(
Buffer.from(accessToken.split('.')[1], 'base64url').toString()
);
const ns = 'https://gw-marketplace/';
return {
roles: payload[`${ns}roles`] ?? [],
orgId: payload[`${ns}orgId`] ?? null,
gwUserId: payload[`${ns}gwUserId`] ?? null,
gwApplicationId: payload[`${ns}gwApplicationId`] ?? null,
};
}
Session Revocation
Revoke sessions using the token revocation endpoint:
async function revokeSession(refreshToken: string) {
await fetch(`${AUTH0_BASE_URL}/oauth/revoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
token: refreshToken,
}),
});
}
Management API v2 Reference
Base URL: https://{AUTH0_DOMAIN}/api/v2
| Operation | Method | Endpoint |
|---|---|---|
| Search Users | GET | /users?q={query}&search_engine=v3 |
| Create User | POST | /users |
| Update User | PATCH | /users/{id} |
| Get User Roles | GET | /users/{id}/roles |
| Assign Roles | POST | /users/{id}/roles |
| List Roles | GET | /roles?name_filter={name} |
Search Query Syntax
Auth0 uses Lucene query syntax for user searches:
# Search by app_metadata field
q=app_metadata.gwUserId:"user-abc123"
# Search by email
q=email:"user@example.com"
# Combined search
q=app_metadata.gwUserId:"user-abc123" OR email:"user@example.com"
Common Gotchas
-
Password policy violations: Auth0 requires at least 8 characters with uppercase, lowercase, number, and special character. The
Gw!prefix in the derived password satisfies these requirements. -
Management API rate limits: Auth0 enforces rate limits on the Management API. Implement exponential backoff for 429 responses. Cache the management token to avoid repeated token requests.
-
Namespace required for custom claims: Auth0 requires custom claims in access tokens to be namespaced with a URL (e.g.,
https://gw-marketplace/). Without the namespace, custom claims are silently dropped. -
Password grant must be explicitly enabled: The Resource Owner Password grant is disabled by default. Enable it under Application > Advanced > Grant Types.
-
Connection name matters: The
connectionfield in user creation must match your database connection name exactly. The default isUsername-Password-Authentication. -
search_engine=v3is required: User searches withoutsearch_engine=v3use the legacy search engine which does not supportapp_metadataqueries.