diff --git a/main/.mintlify/skills/acul-screen-generator/SKILL.md b/main/.mintlify/skills/acul-screen-generator/SKILL.md index 54aa917b72..7c890991fa 100644 --- a/main/.mintlify/skills/acul-screen-generator/SKILL.md +++ b/main/.mintlify/skills/acul-screen-generator/SKILL.md @@ -4,6 +4,9 @@ description: Generates complete, branded Auth0 Advanced Custom Universal Login ( license: Apache-2.0 metadata: author: Auth0 + openclaw: + emoji: "🔐" + homepage: https://github.com/auth0/agent-skills --- # ACUL Screen Generator diff --git a/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-id.js b/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-id.js new file mode 100644 index 0000000000..5c21ccdd72 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-id.js @@ -0,0 +1,137 @@ +// ACUL JS — login-id screen boilerplate +// SDK: @auth0/auth0-acul-js +// Customize: apply design tokens, adjust layout, add/remove social providers + +import LoginId from '@auth0/auth0-acul-js/login-id' + +const manager = new LoginId() + +function getIdentifierLabel() { + // Dynamic label based on configured identifiers + const ids = manager.screen.loginIdentifiers ?? [] + if (ids.length === 0) return 'Email or username' + return `Enter your ${ids.join(' or ')}` +} + +function renderSocialButtons() { + const connections = manager.transaction.alternateConnections ?? [] + if (!connections.length) return '' + + const buttons = connections.map(conn => ` + + `).join('') + + return ` +
+ ${manager.screen.texts?.separatorText ?? 'Or'} +
+
${buttons}
+ ` +} + +function renderErrors() { + if (!manager.transaction.hasErrors) return '' + const msgs = manager.getErrors().map(e => `

${e.message}

`).join('') + return `` +} + +function render() { + const container = document.getElementById('app') + container.innerHTML = ` +
+
+
+ +

+ ${manager.screen.texts?.title ?? 'Log in'} +

+ + ${renderErrors()} + +
+
+ + +
+ + ${manager.screen.isCaptchaAvailable ? ` +
+ + +
+ ` : ''} + + +
+ + ${manager.screen.isPasskeyEnabled ? ` + + ` : ''} + + ${renderSocialButtons()} + + +
+
+ ` + + attachEventListeners() +} + +function attachEventListeners() { + // Form submit + document.getElementById('login-id-form')?.addEventListener('submit', async (e) => { + e.preventDefault() + const submitBtn = document.getElementById('submit-btn') + submitBtn.disabled = true + submitBtn.textContent = 'Loading...' + + const username = document.getElementById('username').value + const captcha = document.getElementById('captcha')?.value + + await manager.login({ username, captcha: captcha || undefined }) + + submitBtn.disabled = false + submitBtn.textContent = manager.screen.texts?.buttonText ?? 'Continue' + }) + + // Passkey + document.getElementById('passkey-btn')?.addEventListener('click', async () => { + await manager.passkeyLogin() + }) + + // Social login + document.querySelectorAll('.acul-social-btn').forEach(btn => { + btn.addEventListener('click', async () => { + await manager.federatedLogin({ connection: btn.dataset.connection }) + }) + }) +} + +render() diff --git a/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-password.js b/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-password.js new file mode 100644 index 0000000000..f8d16bda48 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/js-templates/login-password.js @@ -0,0 +1,138 @@ +// ACUL JS — login-password screen boilerplate +// SDK: @auth0/auth0-acul-js +// Customize: apply design tokens, adjust layout + +import LoginPassword from '@auth0/auth0-acul-js/login-password' + +const manager = new LoginPassword() + +function renderErrors() { + if (!manager.transaction.hasErrors) return '' + const msgs = manager.getErrors().map(e => `

${e.message}

`).join('') + return `` +} + +function renderSocialButtons() { + const connections = manager.transaction.alternateConnections ?? [] + if (!connections.length) return '' + + return ` +
+ ${manager.screen.texts?.separatorText ?? 'Or'} +
+ ${connections.map(conn => ` + + `).join('')} + ` +} + +function render() { + const container = document.getElementById('app') + container.innerHTML = ` +
+
+
+ +

+ ${manager.screen.texts?.title ?? 'Enter your password'} +

+ + ${renderErrors()} + +
+
+ +
+ + +
+
+ + ${manager.screen.isCaptchaAvailable ? ` +
+ + +
+ ` : ''} + + +
+ + ${manager.screen.isPasskeyEnabled ? ` + + ` : ''} + + ${renderSocialButtons()} + + +
+
+ ` + + attachEventListeners() +} + +function attachEventListeners() { + document.getElementById('login-password-form')?.addEventListener('submit', async (e) => { + e.preventDefault() + const submitBtn = document.getElementById('submit-btn') + submitBtn.disabled = true + submitBtn.textContent = 'Loading...' + + const password = document.getElementById('password').value + const captcha = document.getElementById('captcha')?.value + + await manager.login({ password, captcha: captcha || undefined }) + + submitBtn.disabled = false + submitBtn.textContent = manager.screen.texts?.buttonText ?? 'Log in' + }) + + document.getElementById('toggle-password')?.addEventListener('click', () => { + const input = document.getElementById('password') + const btn = document.getElementById('toggle-password') + if (input.type === 'password') { + input.type = 'text' + btn.textContent = 'Hide' + } else { + input.type = 'password' + btn.textContent = 'Show' + } + }) + + document.getElementById('passkey-btn')?.addEventListener('click', async () => { + await manager.passkeyLogin() + }) + + document.querySelectorAll('.acul-social-btn').forEach(btn => { + btn.addEventListener('click', async () => { + await manager.federatedLogin({ connection: btn.dataset.connection }) + }) + }) +} + +render() diff --git a/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-id.tsx b/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-id.tsx new file mode 100644 index 0000000000..4b25679e04 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-id.tsx @@ -0,0 +1,157 @@ +// ACUL React — login-id screen boilerplate +// SDK: @auth0/auth0-acul-react +// Customize: apply design tokens, adjust layout, add/remove social providers + +import React, { useState } from 'react' +import { + useScreen, + useTransaction, + useErrors, + useLoginIdentifiers, + login, + federatedLogin, + passkeyLogin, +} from '@auth0/auth0-acul-react/login-id' +// Import your theme: CSS Modules, Tailwind classes, or styled-components + +interface LoginIdScreenProps {} + +export const LoginIdScreen: React.FC = () => { + // SDK hooks + const screen = useScreen() + const { alternateConnections } = useTransaction() + const { hasErrors, errors } = useErrors() + const identifiers = useLoginIdentifiers() + + // Local state + const [username, setUsername] = useState('') + const [captcha, setCaptcha] = useState('') + const [loading, setLoading] = useState(false) + + // Handlers + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + await login({ username, captcha: captcha || undefined }) + setLoading(false) + } + + const handleSocial = async (connectionName: string) => { + await federatedLogin({ connection: connectionName }) + } + + // Dynamic label from configured identifiers + const identifierLabel = identifiers?.length + ? `Enter your ${identifiers.join(' or ')}` + : 'Email or username' + + return ( +
+
+ {/* Logo slot */} +
+ + {/* Heading */} +

+ {screen.texts?.title ?? 'Log in'} +

+ {screen.texts?.description && ( +

{screen.texts.description}

+ )} + + {/* Error banner */} + {hasErrors && ( +
+ {errors.map((err, i) => ( +

{err.message}

+ ))} +
+ )} + + {/* Login form */} +
+
+ + setUsername(e.target.value)} + required + /> +
+ + {/* Captcha (only if configured) */} + {screen.isCaptchaAvailable && ( +
+ + setCaptcha(e.target.value)} + /> +
+ )} + + +
+ + {/* Passkey (only if enabled) */} + {screen.isPasskeyEnabled && ( + + )} + + {/* Social login */} + {alternateConnections && alternateConnections.length > 0 && ( + <> +
+ {screen.texts?.separatorText ?? 'Or'} +
+
+ {alternateConnections.map(conn => ( + + ))} +
+ + )} + + {/* Footer links */} + +
+
+ ) +} diff --git a/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-password.tsx b/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-password.tsx new file mode 100644 index 0000000000..4cc252bb47 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/react-templates/login-password.tsx @@ -0,0 +1,132 @@ +// ACUL React — login-password screen boilerplate +// SDK: @auth0/auth0-acul-react +// Customize: apply design tokens, adjust layout, add/remove social providers + +import React, { useState } from 'react' +import { + useScreen, + useTransaction, + useErrors, + login, + federatedLogin, + passkeyLogin, +} from '@auth0/auth0-acul-react/login-password' + +interface LoginPasswordScreenProps {} + +export const LoginPasswordScreen: React.FC = () => { + const screen = useScreen() + const { alternateConnections } = useTransaction() + const { hasErrors, errors } = useErrors() + + const [password, setPassword] = useState('') + const [captcha, setCaptcha] = useState('') + const [loading, setLoading] = useState(false) + const [showPassword, setShowPassword] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + await login({ password, captcha: captcha || undefined }) + setLoading(false) + } + + return ( +
+
+
+ +

+ {screen.texts?.title ?? 'Enter your password'} +

+ + {hasErrors && ( +
+ {errors.map((err, i) =>

{err.message}

)} +
+ )} + +
+
+ +
+ setPassword(e.target.value)} + required + /> + +
+
+ + {screen.isCaptchaAvailable && ( +
+ + setCaptcha(e.target.value)} + /> +
+ )} + + +
+ + {screen.isPasskeyEnabled && ( + + )} + + {alternateConnections && alternateConnections.length > 0 && ( + <> +
+ {screen.texts?.separatorText ?? 'Or'} +
+ {alternateConnections.map(conn => ( + + ))} + + )} + + +
+
+ ) +} diff --git a/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/globals.css b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/globals.css new file mode 100644 index 0000000000..98240077bd --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/globals.css @@ -0,0 +1,177 @@ +/* ACUL Screen Generator — Plain CSS globals template + Replace {{TOKEN}} placeholders with extracted design tokens from Phase 5 + Import once in your entry point: import './styles/globals.css' */ + +:root { + --color-primary: {{COLOR_PRIMARY}}; + --color-primary-hover: {{COLOR_PRIMARY_HOVER}}; + --color-primary-text: {{COLOR_PRIMARY_TEXT}}; + --color-background: {{COLOR_BACKGROUND}}; + --color-surface: {{COLOR_SURFACE}}; + --color-text-primary: {{COLOR_TEXT_PRIMARY}}; + --color-text-secondary: {{COLOR_TEXT_SECONDARY}}; + --color-text-placeholder: {{COLOR_TEXT_PLACEHOLDER}}; + --color-border: {{COLOR_BORDER}}; + --color-border-focus: var(--color-primary); + --color-error: {{COLOR_ERROR}}; + --color-error-bg: {{COLOR_ERROR_BG}}; + --color-success: {{COLOR_SUCCESS}}; + --font-family: '{{FONT_FAMILY}}', ui-sans-serif, system-ui, sans-serif; + --radius-card: {{RADIUS_CARD}}; + --radius-input: {{RADIUS_INPUT}}; + --radius-btn: {{RADIUS_BTN}}; + --space-card-padding: {{SPACE_CARD_PADDING}}; + --shadow-card: 0 1px 3px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.06); + --shadow-input-focus: 0 0 0 3px {{COLOR_PRIMARY_FOCUS_RING}}; +} + +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--font-family); + background: var(--color-background); + color: var(--color-text-primary); +} + +/* Shared layout utilities */ +.acul-page-wrapper { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: var(--color-background); +} + +.acul-card { + width: 100%; + max-width: 400px; + background: var(--color-surface); + border-radius: var(--radius-card); + padding: var(--space-card-padding); + box-shadow: var(--shadow-card); +} + +.acul-input { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius-input); + font-family: var(--font-family); + font-size: 0.875rem; + color: var(--color-text-primary); + background: var(--color-surface); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.acul-input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: var(--shadow-input-focus); +} + +.acul-input::placeholder { + color: var(--color-text-placeholder); +} + +.acul-btn-primary { + width: 100%; + padding: 10px 16px; + background: var(--color-primary); + color: var(--color-primary-text); + border: none; + border-radius: var(--radius-btn); + font-family: var(--font-family); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.acul-btn-primary:hover:not(:disabled) { + background: var(--color-primary-hover); +} + +.acul-btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.acul-error-banner { + padding: 10px 12px; + background: var(--color-error-bg); + border: 1px solid var(--color-error); + border-radius: var(--radius-input); + color: var(--color-error); + font-size: 0.875rem; + margin-bottom: 16px; +} + +.acul-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); + margin-bottom: 4px; +} + +.acul-social-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-btn); + font-family: var(--font-family); + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); + cursor: pointer; + transition: background 0.15s; + margin-bottom: 8px; +} + +.acul-social-btn:hover { + background: var(--color-background); +} + +.acul-divider { + display: flex; + align-items: center; + gap: 12px; + margin: 20px 0; +} + +.acul-divider::before, +.acul-divider::after { + content: ''; + flex: 1; + height: 1px; + background: var(--color-border); +} + +.acul-divider span { + font-size: 0.8125rem; + color: var(--color-text-secondary); +} + +.acul-footer-links { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 20px; + font-size: 0.8125rem; +} + +.acul-footer-links a { + color: var(--color-primary); + text-decoration: none; +} + +.acul-footer-links a:hover { + text-decoration: underline; +} diff --git a/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tailwind.config.ts b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tailwind.config.ts new file mode 100644 index 0000000000..69a6e8a762 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tailwind.config.ts @@ -0,0 +1,54 @@ +import type { Config } from 'tailwindcss' + +// ACUL Screen Generator — Tailwind theme template +// Replace token values with extracted design tokens from Phase 5 + +const tokens = { + primary: '{{COLOR_PRIMARY}}', // e.g., '#4F46E5' + primaryHover: '{{COLOR_PRIMARY_HOVER}}', // e.g., '#4338CA' + primaryText: '{{COLOR_PRIMARY_TEXT}}', // e.g., '#FFFFFF' + background: '{{COLOR_BACKGROUND}}', // e.g., '#F9FAFB' + surface: '{{COLOR_SURFACE}}', // e.g., '#FFFFFF' + textPrimary: '{{COLOR_TEXT_PRIMARY}}', // e.g., '#111827' + textSecondary: '{{COLOR_TEXT_SECONDARY}}', // e.g., '#6B7280' + border: '{{COLOR_BORDER}}', // e.g., '#E5E7EB' + error: '{{COLOR_ERROR}}', // e.g., '#EF4444' + success: '{{COLOR_SUCCESS}}', // e.g., '#22C55E' + radiusCard: '{{RADIUS_CARD}}', // e.g., '12px' + radiusInput: '{{RADIUS_INPUT}}', // e.g., '8px' + radiusBtn: '{{RADIUS_BTN}}', // e.g., '8px' +} + +export default { + content: ['./src/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + brand: { + primary: tokens.primary, + 'primary-hover': tokens.primaryHover, + 'primary-text': tokens.primaryText, + background: tokens.background, + surface: tokens.surface, + 'text-primary': tokens.textPrimary, + 'text-secondary': tokens.textSecondary, + border: tokens.border, + error: tokens.error, + success: tokens.success, + }, + }, + borderRadius: { + card: tokens.radiusCard, + input: tokens.radiusInput, + btn: tokens.radiusBtn, + }, + boxShadow: { + card: '0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06)', + }, + fontFamily: { + sans: ['{{FONT_FAMILY}}', 'ui-sans-serif', 'system-ui', 'sans-serif'], + }, + }, + }, + plugins: [], +} satisfies Config diff --git a/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/theme-provider.ts b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/theme-provider.ts new file mode 100644 index 0000000000..d87c0265f3 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/theme-provider.ts @@ -0,0 +1,52 @@ +// ACUL Screen Generator — styled-components ThemeProvider template +// Replace {{TOKEN}} placeholders with extracted design tokens from Phase 5 + +export const theme = { + colors: { + primary: '{{COLOR_PRIMARY}}', // e.g., '#4F46E5' + primaryHover: '{{COLOR_PRIMARY_HOVER}}', // e.g., '#4338CA' + primaryText: '{{COLOR_PRIMARY_TEXT}}', // e.g., '#FFFFFF' + background: '{{COLOR_BACKGROUND}}', // e.g., '#F9FAFB' + surface: '{{COLOR_SURFACE}}', // e.g., '#FFFFFF' + textPrimary: '{{COLOR_TEXT_PRIMARY}}', // e.g., '#111827' + textSecondary: '{{COLOR_TEXT_SECONDARY}}', // e.g., '#6B7280' + textPlaceholder: '{{COLOR_TEXT_PLACEHOLDER}}',// e.g., '#9CA3AF' + border: '{{COLOR_BORDER}}', // e.g., '#E5E7EB' + error: '{{COLOR_ERROR}}', // e.g., '#EF4444' + errorBg: '{{COLOR_ERROR_BG}}', // e.g., '#FEF2F2' + success: '{{COLOR_SUCCESS}}', // e.g., '#22C55E' + }, + typography: { + fontFamily: "'{{FONT_FAMILY}}', ui-sans-serif, system-ui, sans-serif", + fontSizeHeading:'{{FONT_SIZE_HEADING}}', // e.g., '1.5rem' + fontSizeBody: '{{FONT_SIZE_BODY}}', // e.g., '0.875rem' + fontWeightBold: '{{FONT_WEIGHT_HEADING}}', // e.g., '700' + }, + radii: { + card: '{{RADIUS_CARD}}', // e.g., '12px' + input: '{{RADIUS_INPUT}}', // e.g., '8px' + btn: '{{RADIUS_BTN}}', // e.g., '8px' + full: '9999px', + }, + spacing: { + cardPadding: '{{SPACE_CARD_PADDING}}', // e.g., '32px' + formGap: '{{SPACE_FORM_GAP}}', // e.g., '16px' + }, + shadows: { + card: '0 1px 3px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.06)', + inputFocus: '0 0 0 3px {{COLOR_PRIMARY_FOCUS_RING}}', + }, +} as const + +export type Theme = typeof theme + +// Usage in app entry point: +// import { ThemeProvider } from 'styled-components' +// import { theme } from './theme' +// + +// Usage in styled component: +// const Button = styled.button` +// background: ${({ theme }) => theme.colors.primary}; +// border-radius: ${({ theme }) => theme.radii.btn}; +// ` diff --git a/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tokens.css b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tokens.css new file mode 100644 index 0000000000..c96b7bb7b0 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/assets/theme-templates/tokens.css @@ -0,0 +1,60 @@ +/* ACUL Screen Generator — CSS Custom Properties token template + Replace {{TOKEN}} placeholders with extracted design tokens from Phase 5 + Import this file once in your entry point or each screen's module CSS */ + +:root { + /* Brand colors */ + --color-primary: {{COLOR_PRIMARY}}; /* e.g., #4F46E5 */ + --color-primary-hover: {{COLOR_PRIMARY_HOVER}}; /* e.g., #4338CA */ + --color-primary-text: {{COLOR_PRIMARY_TEXT}}; /* e.g., #FFFFFF */ + + /* Backgrounds */ + --color-background: {{COLOR_BACKGROUND}}; /* page bg, e.g., #F9FAFB */ + --color-surface: {{COLOR_SURFACE}}; /* card bg, e.g., #FFFFFF */ + --color-surface-raised: {{COLOR_SURFACE_RAISED}}; /* elevated card, e.g., #FFFFFF */ + + /* Text */ + --color-text-primary: {{COLOR_TEXT_PRIMARY}}; /* headings, e.g., #111827 */ + --color-text-secondary: {{COLOR_TEXT_SECONDARY}}; /* labels, e.g., #6B7280 */ + --color-text-placeholder:{{COLOR_TEXT_PLACEHOLDER}};/* e.g., #9CA3AF */ + + /* Borders */ + --color-border: {{COLOR_BORDER}}; /* input borders, e.g., #E5E7EB */ + --color-border-focus: var(--color-primary); + + /* States */ + --color-error: {{COLOR_ERROR}}; /* e.g., #EF4444 */ + --color-error-bg: {{COLOR_ERROR_BG}}; /* e.g., #FEF2F2 */ + --color-success: {{COLOR_SUCCESS}}; /* e.g., #22C55E */ + + /* Spacing */ + --space-card-padding: {{SPACE_CARD_PADDING}}; /* e.g., 32px */ + --space-form-gap: {{SPACE_FORM_GAP}}; /* e.g., 16px */ + + /* Typography */ + --font-family: {{FONT_FAMILY}}, ui-sans-serif, system-ui, sans-serif; + --font-size-heading: {{FONT_SIZE_HEADING}}; /* e.g., 1.5rem */ + --font-size-body: {{FONT_SIZE_BODY}}; /* e.g., 0.875rem */ + --font-weight-heading: {{FONT_WEIGHT_HEADING}}; /* e.g., 700 */ + + /* Border radius */ + --radius-card: {{RADIUS_CARD}}; /* e.g., 12px */ + --radius-input: {{RADIUS_INPUT}}; /* e.g., 8px */ + --radius-btn: {{RADIUS_BTN}}; /* e.g., 8px */ + --radius-full: 9999px; + + /* Shadows */ + --shadow-card: 0 1px 3px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.06); + --shadow-input-focus: 0 0 0 3px {{COLOR_PRIMARY_FOCUS_RING}}; /* primary at 20% */ +} + +/* Base resets for ACUL screens */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--font-family); + font-size: var(--font-size-body); + color: var(--color-text-primary); + background: var(--color-background); +} diff --git a/main/.mintlify/skills/acul-screen-generator/references/acul-js-sdk.md b/main/.mintlify/skills/acul-screen-generator/references/acul-js-sdk.md new file mode 100644 index 0000000000..0acebe4370 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/acul-js-sdk.md @@ -0,0 +1,184 @@ +# Auth0 ACUL JS SDK Reference + +Package: `@auth0/auth0-acul-js` + +Uses a manager class pattern. Each screen exports a default class with methods matching available actions. + +--- + +## Import Pattern + +```typescript +import LoginId from '@auth0/auth0-acul-js/login-id' + +const manager = new LoginId() +``` + +Replace `login-id` with the screen name. Class name is PascalCase of the screen name. + +--- + +## Manager Instance Properties + +```typescript +manager.transaction.hasErrors // boolean +manager.transaction.alternateConnections // social/enterprise connections array +manager.transaction.connection // primary connection +manager.getErrors() // returns array of { code, message } +manager.screen.texts // localised text strings +manager.screen.name // current screen name +manager.screen.isCaptchaAvailable // boolean +manager.screen.isPasskeyEnabled // boolean +``` + +--- + +## Common Methods by Screen + +### Login screens +```typescript +// login-id +const manager = new LoginId() +await manager.login({ username: 'user@example.com', captcha: '...' }) +await manager.federatedLogin({ connection: 'google-oauth2' }) +await manager.passkeyLogin() +await manager.pickCountryCode() + +// login-password +const manager = new LoginPassword() +await manager.login({ password: 'secret', captcha: '...' }) +await manager.federatedLogin({ connection: 'google-oauth2' }) +await manager.passkeyLogin() +``` + +### Signup screens +```typescript +const manager = new Signup() +await manager.signup({ email: 'user@example.com', password: 'secret' }) +await manager.federatedLogin({ connection: 'google-oauth2' }) +``` + +### MFA screens +```typescript +// mfa-otp-challenge +const manager = new MfaOtpChallenge() +await manager.continueWithMfaOtp({ code: '123456' }) + +// mfa-sms-challenge +const manager = new MfaSmsChallenge() +await manager.continueWithMfaSms({ code: '123456' }) + +// mfa-email-challenge +const manager = new MfaEmailChallenge() +await manager.continueWithEmail({ code: '123456' }) +``` + +### Password reset screens +```typescript +const manager = new ResetPasswordRequest() +await manager.requestPasswordReset({ email: 'user@example.com' }) + +const manager = new ResetPassword() +await manager.resetPassword({ password: 'newpass', confirmPassword: 'newpass' }) +``` + +--- + +## Standard Component Structure (Vanilla JS) + +```javascript +import LoginId from '@auth0/auth0-acul-js/login-id' + +const manager = new LoginId() + +function render() { + const container = document.getElementById('app') + container.innerHTML = ` +
+
+
+

${manager.screen.texts?.title ?? 'Log in'}

+ + ${manager.transaction.hasErrors ? ` +
+ ${manager.getErrors().map(e => `

${e.message}

`).join('')} +
+ ` : ''} + +
+ + + + ${manager.screen.isCaptchaAvailable ? ` + + ` : ''} + + +
+ + ${manager.transaction.alternateConnections?.length ? ` +
Or
+ ${manager.transaction.alternateConnections.map(conn => ` + + `).join('')} + ` : ''} + + +
+
+ ` + + // Attach event listeners after render + document.getElementById('login-form').addEventListener('submit', async (e) => { + e.preventDefault() + const username = document.getElementById('username').value + const captcha = document.getElementById('captcha')?.value + await manager.login({ username, captcha }) + }) + + document.querySelectorAll('.social-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const connection = btn.dataset.connection + await manager.federatedLogin({ connection }) + }) + }) + + if (manager.screen.isPasskeyEnabled) { + // Passkey button handler + document.getElementById('passkey-btn')?.addEventListener('click', async () => { + await manager.passkeyLogin() + }) + } +} + +render() +``` + +--- + +## Manager Class Name Reference + +| Screen | Import path | Class name | +|--------|-------------|------------| +| login-id | `@auth0/auth0-acul-js/login-id` | `LoginId` | +| login-password | `@auth0/auth0-acul-js/login-password` | `LoginPassword` | +| signup | `@auth0/auth0-acul-js/signup` | `Signup` | +| signup-id | `@auth0/auth0-acul-js/signup-id` | `SignupId` | +| signup-password | `@auth0/auth0-acul-js/signup-password` | `SignupPassword` | +| mfa-otp-challenge | `@auth0/auth0-acul-js/mfa-otp-challenge` | `MfaOtpChallenge` | +| mfa-email-challenge | `@auth0/auth0-acul-js/mfa-email-challenge` | `MfaEmailChallenge` | +| mfa-sms-challenge | `@auth0/auth0-acul-js/mfa-sms-challenge` | `MfaSmsChallenge` | +| reset-password-request | `@auth0/auth0-acul-js/reset-password-request` | `ResetPasswordRequest` | +| reset-password | `@auth0/auth0-acul-js/reset-password` | `ResetPassword` | +| passkey-enrollment | `@auth0/auth0-acul-js/passkey-enrollment` | `PasskeyEnrollment` | + +For full screen list and fallback URLs → see `screen-catalog.md`. diff --git a/main/.mintlify/skills/acul-screen-generator/references/acul-react-sdk.md b/main/.mintlify/skills/acul-screen-generator/references/acul-react-sdk.md new file mode 100644 index 0000000000..903cba79b0 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/acul-react-sdk.md @@ -0,0 +1,234 @@ +# Auth0 ACUL React SDK Reference + +Package: `@auth0/auth0-acul-react` + +Each screen has its own import path. Import hooks and action functions from the screen-specific path. + +--- + +## Import Pattern + +```tsx +import { + useScreen, + useTransaction, + useErrors, + useLoginIdentifiers, + login, + federatedLogin, + passkeyLogin, +} from '@auth0/auth0-acul-react/login-id' +``` + +Replace `login-id` with the screen name (e.g., `signup`, `login-password`, `mfa-otp-challenge`). + +--- + +## Common Hooks + +### `useScreen()` +Returns screen configuration and localised text strings. +```tsx +const screen = useScreen() +screen.texts?.title // screen heading text +screen.texts?.description // subheading/description +screen.name // current screen name +screen.links?.signUp // navigation link to signup +screen.links?.resetPassword // navigation link to password reset +screen.links?.login // navigation link to login +``` + +### `useTransaction()` +Returns transaction state and available connections. +```tsx +const { hasErrors, alternateConnections, connection } = useTransaction() +alternateConnections // array of social/enterprise connections +connection.name // primary connection name +``` + +### `useErrors()` +Returns error state from the current transaction. +```tsx +const { hasErrors, errors } = useErrors() +// errors: array of { code, message } +``` + +### `useLoginIdentifiers()` +Returns active identifier types for dynamic label generation. +```tsx +const identifiers = useLoginIdentifiers() +// ['email', 'username'] → "Enter your email or username" +``` + +--- + +## Action Functions + +Action functions are imported alongside hooks and called from event handlers. + +### Authentication actions +```tsx +login({ username, password, captcha }) // login-id, login-password +federatedLogin({ connection: 'google-oauth2' }) // social login +passkeyLogin() // passkey prompt (native dialog) +pickCountryCode() // phone country code picker +``` + +### Signup actions +```tsx +signup({ email, password, username }) +``` + +### MFA actions +```tsx +continueWithMfaOtp({ code }) +continueWithMfaSms({ code }) +continueWithEmail({ code }) +enrollWithTotp({ code }) +``` + +### Password reset actions +```tsx +requestPasswordReset({ email }) +resetPassword({ password, confirmPassword }) +``` + +### Session actions +```tsx +logout() +``` + +--- + +## Standard Component Structure + +```tsx +import React, { useState } from 'react' +import { + useScreen, useTransaction, useErrors, + login, federatedLogin, passkeyLogin, +} from '@auth0/auth0-acul-react/login-id' + +export const LoginIdScreen: React.FC = () => { + // 1. SDK hooks + const screen = useScreen() + const { alternateConnections } = useTransaction() + const { hasErrors, errors } = useErrors() + + // 2. Local state + const [username, setUsername] = useState('') + const [loading, setLoading] = useState(false) + const [captcha, setCaptcha] = useState('') + + // 3. Event handlers + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + await login({ username, captcha }) + setLoading(false) + } + + const handleSocial = async (connectionName: string) => { + await federatedLogin({ connection: connectionName }) + } + + // 4. JSX + return ( +
+
+ {/* Logo slot */} +
+ + {/* Title from screen config */} +

{screen.texts?.title ?? 'Log in'}

+ + {/* Error banner */} + {hasErrors && ( +
+ {errors.map(e =>

{e.message}

)} +
+ )} + + {/* Form */} +
+ + setUsername(e.target.value)} + /> + +
+ + {/* Social login */} + {alternateConnections?.length > 0 && ( + <> +
Or
+ {alternateConnections.map(conn => ( + + ))} + + )} + + {/* Footer links */} + +
+
+ ) +} +``` + +--- + +## Conditional Features + +```tsx +// Captcha (check if configured) +{screen.isCaptchaAvailable && ( + setCaptcha(e.target.value)} /> +)} + +// Passkey button +{screen.isPasskeyEnabled && ( + +)} + +// Country code for phone flows +{screen.isPhoneFlow && ( + +)} +``` + +--- + +## Screen-Specific Imports Quick Reference + +| Screen | Import path | +|--------|-------------| +| login-id | `@auth0/auth0-acul-react/login-id` | +| login-password | `@auth0/auth0-acul-react/login-password` | +| signup | `@auth0/auth0-acul-react/signup` | +| signup-id | `@auth0/auth0-acul-react/signup-id` | +| signup-password | `@auth0/auth0-acul-react/signup-password` | +| mfa-otp-challenge | `@auth0/auth0-acul-react/mfa-otp-challenge` | +| mfa-email-challenge | `@auth0/auth0-acul-react/mfa-email-challenge` | +| mfa-sms-challenge | `@auth0/auth0-acul-react/mfa-sms-challenge` | +| reset-password-request | `@auth0/auth0-acul-react/reset-password-request` | +| reset-password | `@auth0/auth0-acul-react/reset-password` | +| passkey-enrollment | `@auth0/auth0-acul-react/passkey-enrollment` | + +For full screen list and fallback URLs → see `screen-catalog.md`. diff --git a/main/.mintlify/skills/acul-screen-generator/references/cli-commands.md b/main/.mintlify/skills/acul-screen-generator/references/cli-commands.md new file mode 100644 index 0000000000..8058d8ac5e --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/cli-commands.md @@ -0,0 +1,154 @@ +# Auth0 ACUL CLI Commands Reference + +Full reference for `auth0 acul` commands. Requires Auth0 CLI installed (`brew install auth0`). + +--- + +## Authentication + +```bash +auth0 login # authenticate with Auth0 tenant +auth0 login --tenant # authenticate to a specific tenant +``` + +--- + +## auth0 acul init + +Generates a new ACUL project from a template. + +```bash +auth0 acul init +auth0 acul init -t react -s login,signup +auth0 acul init -t react -s login,login-id,login-password,signup,reset-password +``` + +| Flag | Short | Description | +|------|-------|-------------| +| `--template` | `-t` | Framework template: `react` or `js` | +| `--screens` | `-s` | Comma-separated screen list | +| `--tenant` | | Target a specific tenant | +| `--no-input` | | Disable interactive prompts | + +Creates `acul_config.json` in the project directory — required for all subsequent commands. + +--- + +## auth0 acul screen add + +Adds screens to an existing ACUL project. Project must already be initialized. + +```bash +auth0 acul screen add -d +auth0 acul screen add login-id login-password -d ./acul_app +auth0 acul screen add mfa-otp-challenge -d ./my-project +``` + +| Flag | Short | Description | +|------|-------|-------------| +| `--dir` | `-d` | Path to project directory (must contain `acul_config.json`) | +| `--tenant` | | Target a specific tenant | + +**ON ERROR:** Fall back to SDK examples — see `screen-catalog.md` for URLs. + +--- + +## auth0 acul config + +### List configurations + +```bash +auth0 acul config list +auth0 acul config list --rendering-mode advanced +auth0 acul config list --screen login-id +auth0 acul config list --prompt login --rendering-mode advanced --fields head_tags,context_configuration +``` + +| Flag | Description | +|------|-------------| +| `--prompt` | Filter by Universal Login prompt | +| `--rendering-mode` | Filter by mode: `advanced` or `standard` | +| `--screen` | Filter by screen name | +| `--fields` | Comma-separated fields to include | +| `--json` | Output as JSON | +| `--page` | Page index (starts at 0) | +| `--per-page` | Results per page (default 50, max 100) | + +### Get screen config + +```bash +auth0 acul config get +auth0 acul config get login-id -f ./acul_config/login-id.json +auth0 acul config get signup-id --file settings.json +``` + +| Flag | Short | Description | +|------|-------|-------------| +| `--file` | `-f` | Save config to file path | + +### Set screen config + +```bash +auth0 acul config set +auth0 acul config set login-id --file settings.json +``` + +### Generate config stub + +```bash +auth0 acul config generate +auth0 acul config generate login-id --file login-settings.json +``` + +Generates a stub `.json` config file for a screen. Use after `screen add` or during scratch setup. + +--- + +## auth0 acul dev + +Starts development mode with automatic build watching. + +```bash +# Local preview (no tenant connection) +auth0 acul dev -p 3000 +auth0 acul dev -p 3000 -d ./my-project + +# Connected mode — updates rendering settings on tenant (stage/dev only) +auth0 acul dev --connected -s login-id +auth0 acul dev -c -s login-id,signup -d ./my-project +``` + +| Flag | Short | Description | +|------|-------|-------------| +| `--port` | `-p` | Local dev server port (required) | +| `--dir` | `-d` | ACUL project directory path | +| `--connected` | `-c` | Connected mode: syncs to tenant | +| `--screens` | `-s` | Specific screens to develop | + +⚠️ **Connected mode warning:** only use on stage/dev tenants, never production. + +--- + +## Typical Workflows + +### Scratch setup +```bash +auth0 login +auth0 acul init my-app -t react -s login-id,login-password,signup +cd my-app +auth0 acul config generate login-id +auth0 acul dev -p 3000 +``` + +### Add a screen to existing project +```bash +auth0 acul screen add mfa-otp-challenge -d ./my-app +auth0 acul config generate mfa-otp-challenge -f ./my-app/acul_config/mfa-otp-challenge.json +auth0 acul dev -p 3000 -d ./my-app +``` + +### Inspect current tenant ACUL state +```bash +auth0 acul config list --rendering-mode advanced --json +auth0 acul config get login-id -f login-id-current.json +``` diff --git a/main/.mintlify/skills/acul-screen-generator/references/screen-catalog.md b/main/.mintlify/skills/acul-screen-generator/references/screen-catalog.md new file mode 100644 index 0000000000..899dc22943 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/screen-catalog.md @@ -0,0 +1,321 @@ +# ACUL Screen Catalog + +Complete reference for all 68 React + 71 JS ACUL screens with their reference sources, SDK callbacks, and URLs. + +**Reference priority per screen:** +1. **auth0-acul-samples** if `Samples` column = ✅ → fetch full modular implementation +2. **SDK examples** if `Samples` column = ❌ → fetch the markdown example for SDK usage +3. **assets/templates** — structural pattern only, never for hooks/actions + +The `Samples` column marks which screens have a complete implementation in `auth0-acul-samples`. + +> **Note:** `continueMethod()` in the tables below is a placeholder — the actual method name is screen-specific (e.g., `continueWithMfaOtp()`, `continueWithMfaSms()`). Always fetch the SDK example to get the exact method name and payload shape. + +## Table of Contents +1. [URL Patterns](#url-patterns) +2. [Hook Patterns](#hook-patterns) +3. [Login & Authentication](#login--authentication) +4. [Signup & Registration](#signup--registration) +5. [Password Reset](#password-reset) +6. [Password Reset + MFA Challenges](#password-reset--mfa-challenges) +7. [MFA — Enrollment & Options](#mfa--enrollment--options) +8. [MFA — Email](#mfa--email) +9. [MFA — SMS / Voice / Phone](#mfa--sms--voice--phone) +10. [MFA — OTP (TOTP)](#mfa--otp-totp) +11. [MFA — Push Notifications](#mfa--push-notifications) +12. [MFA — WebAuthn](#mfa--webauthn) +13. [MFA — Recovery Codes](#mfa--recovery-codes) +14. [Passkeys](#passkeys) +15. [Identifier Challenges](#identifier-challenges) +16. [Device Authorization](#device-authorization) +17. [Organization Management](#organization-management) +18. [Consent & Security](#consent--security) +19. [Session / Logout](#session--logout) +20. [Email Verification](#email-verification) +21. [JS-Only Screens](#js-only-screens) + +--- + +## URL Patterns + +### auth0-acul-samples (Priority 1) +``` +React: + directory: https://github.com/auth0-samples/auth0-acul-samples/tree/main/react/src/screens/ + index.tsx: https://github.com/auth0-samples/auth0-acul-samples/blob/main/react/src/screens//index.tsx + manager: https://github.com/auth0-samples/auth0-acul-samples/blob/main/react/src/screens//hooks/useManager.ts + +React-JS: + directory: https://github.com/auth0-samples/auth0-acul-samples/tree/main/react-js/src/screens/ + index.tsx: https://github.com/auth0-samples/auth0-acul-samples/blob/main/react-js/src/screens//index.tsx +``` + +### SDK examples (Priority 2) +``` +React: https://github.com/auth0/universal-login/blob/master/packages/auth0-acul-react/examples/.md +JS: https://github.com/auth0/universal-login/blob/master/packages/auth0-acul-js/examples/.md +``` + +--- + +## Hook Patterns + +ACUL screens use two patterns. The reference fetch tells you which applies. + +**Pattern A — Generic hooks** (most login/signup screens): +```tsx +import { useScreen, useTransaction, useErrors, login } from '@auth0/auth0-acul-react/' +const screen = useScreen() +const { alternateConnections } = useTransaction() +``` + +**Pattern B — Screen-specific hook** (most MFA, reset-password-mfa, recovery screens): +```tsx +import { useScreenName, continueMethod } from '@auth0/auth0-acul-react/' +const screen = useScreenName() // e.g., useMfaRecoveryCodeEnrollment() +await continueMethod({ ...payload }) +``` + +**JS — Manager class** (both patterns map to this): +```js +import ScreenClass from '@auth0/auth0-acul-js/' +const manager = new ScreenClass() +await manager.continueMethod({ ...payload }) +``` + +--- + +## Login & Authentication + +| Screen | Samples (React) | Samples (React-JS) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|--------------------|-----------|--------|----------------|-------| +| `login` | ✅ | ✅ | ✅ | ✅ | `login()`, `federatedLogin()` | All-identifier login | +| `login-id` | ✅ | ✅ | ✅ | ✅ | `login()`, `federatedLogin()`, `passkeyLogin()` | Identifier-first step | +| `login-password` | ✅ | ✅ | ✅ | ✅ | `login()`, `federatedLogin()`, `passkeyLogin()` | Password entry step | +| `login-passwordless-email-code` | ✅ | ❌ | ✅ | ✅ | `continueMethod()` | Email OTP | +| `login-passwordless-sms-otp` | ✅ | ❌ | ✅ | ✅ | `continueMethod()` | SMS OTP | +| `login-email-verification` | ❌ | ❌ | ✅ | ✅ | — | Gate screen, no action | + +--- + +## Signup & Registration + +| Screen | Samples (React) | Samples (React-JS) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|--------------------|-----------|--------|----------------|-------| +| `signup` | ✅ | ❌ | ✅ | ✅ | `signup()`, `federatedLogin()` | Combined signup | +| `signup-id` | ✅ | ❌ | ✅ | ✅ | `signup()`, `federatedLogin()` | Identifier-first | +| `signup-password` | ✅ | ❌ | ✅ | ✅ | `signup()` | Password entry | +| `accept-invitation` | ❌ | ❌ | ✅ | ✅ | `signup()` | Org invite | +| `redeem-ticket` | ❌ | ❌ | ✅ | ✅ | — | Ticket-based access | + +--- + +## Password Reset + +| Screen | Samples (React) | Samples (React-JS) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|--------------------|-----------|--------|----------------|-------| +| `reset-password-request` | ✅ | ❌ | ✅ | ✅ | `requestPasswordReset()` | Sends reset email | +| `reset-password-email` | ✅ | ❌ | ✅ | ✅ | — | Email sent confirmation | +| `reset-password` | ✅ | ❌ | ✅ | ✅ | `continueMethod()` | Enter new password | +| `reset-password-success` | ✅ | ❌ | ✅ | ✅ | — | Success state | +| `reset-password-error` | ✅ | ❌ | ✅ | ✅ | — | Error state | + +--- + +## Password Reset + MFA Challenges + +All screens: Pattern B (screen-specific hook + `continueMethod()`). Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `reset-password-mfa-email-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-otp-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-phone-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-push-challenge-push` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-recovery-code-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-sms-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-voice-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-webauthn-platform-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `reset-password-mfa-webauthn-roaming-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## MFA — Enrollment & Options + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|-----------|--------|----------------|-------| +| `mfa-begin-enroll-options` | ✅ | ✅ | ✅ | — | Options list | +| `mfa-login-options` | ✅ | ✅ | ✅ | — | Login method picker | +| `mfa-detect-browser-capabilities` | ❌ | ✅ | ✅ | — | Capability check | +| `mfa-enroll-result` | ✅ | ✅ | ✅ | — | Enrollment confirmation | +| `mfa-country-codes` | ✅ | ✅ | ✅ | `continueMethod()` | Phone country picker | + +--- + +## MFA — Email + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `mfa-email-challenge` | ✅ | ✅ | ✅ | `continueMethod()` | +| `mfa-email-list` | ✅ | ✅ | ✅ | — | + +--- + +## MFA — SMS / Voice / Phone + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `mfa-sms-challenge` | ✅ | ✅ | ✅ | `continueMethod()` | +| `mfa-sms-enrollment` | ✅ | ✅ | ✅ | `continueMethod()` | +| `mfa-sms-list` | ✅ | ✅ | ✅ | — | +| `mfa-voice-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `mfa-voice-enrollment` | ❌ | ✅ | ✅ | `continueMethod()` | +| `mfa-phone-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `mfa-phone-enrollment` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## MFA — OTP (TOTP) + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `mfa-otp-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | +| `mfa-otp-enrollment-qr` | ❌ | ✅ | ✅ | `continueMethod()` | +| `mfa-otp-enrollment-code` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## MFA — Push Notifications + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `mfa-push-welcome` | ✅ | ✅ | ✅ | — | +| `mfa-push-enrollment-qr` | ✅ | ✅ | ✅ | `continueMethod()` | +| `mfa-push-challenge-push` | ✅ | ✅ | ✅ | `continueMethod()` | +| `mfa-push-list` | ✅ | ✅ | ✅ | — | + +--- + +## MFA — WebAuthn + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|-----------|--------|----------------|-------| +| `mfa-webauthn-platform-enrollment` | ❌ | ✅ | ✅ | `submitPasskeyCredential()`, `snoozeEnrollment()`, `refuseEnrollmentOnThisDevice()` | 3 actions | +| `mfa-webauthn-platform-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | | +| `mfa-webauthn-roaming-enrollment` | ❌ | ✅ | ✅ | `continueMethod()` | | +| `mfa-webauthn-roaming-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | | +| `mfa-webauthn-change-key-nickname` | ❌ | ✅ | ✅ | `continueMethod()` | | +| `mfa-webauthn-enrollment-success` | ❌ | ✅ | ✅ | — | Success state | +| `mfa-webauthn-error` | ❌ | ✅ | ✅ | — | Error state | +| `mfa-webauthn-not-available-error` | ❌ | ✅ | ✅ | — | Capability error | + +--- + +## MFA — Recovery Codes + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|-----------|--------|----------------|-------| +| `mfa-recovery-code-enrollment` | ❌ | ✅ | ✅ | `continueMethod({ isCodeCopied })` | Screen-specific hook | +| `mfa-recovery-code-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | | +| `mfa-recovery-code-challenge-new-code` | ❌ | ✅ | ✅ | `continueMethod()` | | + +--- + +## Passkeys + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | Notes | +|--------|-----------------|-----------|--------|----------------|-------| +| `passkey-enrollment` | ✅ | ✅ | ✅ | `submitPasskeyCredential()` | Native dialog | +| `passkey-enrollment-local` | ✅ | ✅ | ✅ | `continueMethod()` | Local device | + +--- + +## Identifier Challenges + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `email-identifier-challenge` | ✅ | ✅ | ✅ | `continueMethod()` | +| `phone-identifier-challenge` | ✅ | ✅ | ✅ | `continueMethod()` | +| `phone-identifier-enrollment` | ✅ | ✅ | ✅ | `continueMethod()` | +| `email-otp-challenge` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## Device Authorization + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `device-code-activation` | ❌ | ✅ | ✅ | `continueMethod()` | +| `device-code-confirmation` | ❌ | ✅ | ✅ | `continueMethod()` | +| `device-code-activation-allowed` | ❌ | ✅ | ✅ | — | +| `device-code-activation-denied` | ❌ | ✅ | ✅ | — | + +--- + +## Organization Management + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `organization-picker` | ❌ | ✅ | ✅ | `continueMethod()` | +| `organization-selection` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## Consent & Security + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `consent` | ❌ | ✅ | ✅ | `continueMethod()` | +| `customized-consent` | ❌ | ✅ | ✅ | `continueMethod()` | +| `interstitial-captcha` | ❌ | ✅ | ✅ | `continueMethod()` | + +--- + +## Session / Logout + +Not in samples — use SDK examples. + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `logout` | ❌ | ✅ | ✅ | `logout()` | +| `logout-aborted` | ❌ | ✅ | ✅ | — | +| `logout-complete` | ❌ | ✅ | ✅ | — | + +--- + +## Email Verification + +| Screen | Samples (React) | SDK React | SDK JS | Primary Action | +|--------|-----------------|-----------|--------|----------------| +| `email-verification-result` | ❌ | ✅ | ✅ | — | + +--- + +## JS-Only Screens + +Only in `@auth0/auth0-acul-js`. No React SDK or samples equivalent. Use JS SDK examples. + +| Screen | Primary Action | Notes | +|--------|----------------|-------| +| `brute-force-protection-unblock` | `unblockAccount()` | Account unblock | +| `brute-force-protection-unblock-success` | — | Success state | +| `brute-force-protection-unblock-failure` | — | Failure state | +| `get-current-screen-options` | — | Utility: read screen config | +| `get-current-theme-options` | — | Utility: read theme config | + +JS SDK example URL: +``` +https://github.com/auth0/universal-login/blob/master/packages/auth0-acul-js/examples/.md +``` diff --git a/main/.mintlify/skills/acul-screen-generator/references/social-providers.md b/main/.mintlify/skills/acul-screen-generator/references/social-providers.md new file mode 100644 index 0000000000..3c49aaff42 --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/social-providers.md @@ -0,0 +1,171 @@ +# Social Login Provider Patterns + +Patterns for rendering social login buttons in ACUL screens. Social connections come from `alternateConnections` on the transaction object — never hardcode connection names. + +--- + +## Data Shape + +```typescript +// From useTransaction() (React) or manager.transaction (JS) +alternateConnections: Array<{ + name: string // e.g., "google-oauth2", "github", "apple" + displayName: string // e.g., "Google", "GitHub", "Apple" + iconUrl?: string // provider icon URL if available + strategy: string // e.g., "google-oauth2", "github", "apple" +}> +``` + +--- + +## React Pattern + +```tsx +import { useTransaction, federatedLogin } from '@auth0/auth0-acul-react/login-id' + +const { alternateConnections } = useTransaction() + +// In JSX +{alternateConnections?.length > 0 && ( +
+
+ Or continue with +
+
+ {alternateConnections.map(conn => ( + + ))} +
+
+)} +``` + +```tsx +const SocialButton: React.FC<{ connection: AlternateConnection }> = ({ connection }) => { + const [loading, setLoading] = useState(false) + + const handleClick = async () => { + setLoading(true) + await federatedLogin({ connection: connection.name }) + setLoading(false) + } + + return ( + + ) +} +``` + +--- + +## JS Pattern + +```javascript +import LoginId from '@auth0/auth0-acul-js/login-id' +const manager = new LoginId() + +function renderSocialButtons() { + const connections = manager.transaction.alternateConnections ?? [] + if (!connections.length) return '' + + return ` + + ` +} + +// Attach handlers after render +document.querySelectorAll('.social-btn').forEach(btn => { + btn.addEventListener('click', async () => { + await manager.federatedLogin({ connection: btn.dataset.connection }) + }) +}) +``` + +--- + +## Provider-Specific Icon SVGs + +Use these inline SVGs when `iconUrl` is unavailable or for consistent brand rendering. + +### Google +```html + + + + + + +``` + +### GitHub +```html + + + +``` + +### Apple +```html + + + +``` + +### Microsoft +```html + + + + + + +``` + +--- + +## Styling the Divider + +```css +.divider { + display: flex; + align-items: center; + gap: 12px; + margin: 16px 0; +} +.divider::before, +.divider::after { + content: ''; + flex: 1; + height: 1px; + background: var(--color-border); +} +.divider span { + color: var(--color-text-secondary); + font-size: 0.875rem; + white-space: nowrap; +} +``` diff --git a/main/.mintlify/skills/acul-screen-generator/references/theming-patterns.md b/main/.mintlify/skills/acul-screen-generator/references/theming-patterns.md new file mode 100644 index 0000000000..315fa4b18f --- /dev/null +++ b/main/.mintlify/skills/acul-screen-generator/references/theming-patterns.md @@ -0,0 +1,165 @@ +# Theming Patterns for ACUL Screens + +--- + +## Design Token Derivation + +When only brand colors are provided (no image), derive the full token set: + +``` +Input: primary color (e.g., #4F46E5) + +Derived tokens: + --color-primary = input hex + --color-primary-hover = primary darkened ~10% (hsl lightness -10) + --color-primary-text = white if primary is dark, else #111827 + + --color-background = #FFFFFF (light) or #0F172A (dark, if brand is dark) + --color-surface = #F9FAFB (light) or #1E293B (dark) + --color-surface-raised = #FFFFFF (light) or #293548 (dark) + + --color-text-primary = #111827 (light) or #F1F5F9 (dark) + --color-text-secondary = #6B7280 (light) or #94A3B8 (dark) + --color-text-placeholder = #9CA3AF + + --color-border = #E5E7EB (light) or #334155 (dark) + --color-border-focus = primary color + + --color-error = #EF4444 + --color-error-bg = #FEF2F2 + --color-success = #22C55E + --color-success-bg = #F0FDF4 + + --radius-sm = 4px + --radius-md = 8px + --radius-lg = 12px + --radius-full = 9999px + + --shadow-card = 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06) + --shadow-input-focus = 0 0 0 3px +``` + +--- + +## Image/Mockup Analysis + +When a screenshot or design mockup is provided, extract: + +1. **Colors** — sample from key areas: + - Page background color + - Card/panel background + - Primary button color + - Input border color + - Text colors (heading, body, placeholder) + - Error state color + +2. **Typography** — identify: + - Font family (match to Google Fonts or system font stack if custom) + - Heading size and weight + - Body text size + - Button text style + +3. **Spatial rhythm** — measure approximate: + - Card padding (compact ~16px / normal ~24px / spacious ~32px) + - Input height (small ~36px / medium ~40px / large ~48px) + - Button border radius (sharp 0px / slight 4px / rounded 8px / pill 9999px) + +4. **Layout type:** + - Centered card (card centered on solid background) + - Full-bleed (edge-to-edge, no visible card) + - Split panel (image/brand on left, form on right) + - Floating card (card with shadow on gradient/image background) + +--- + +## Theme File Patterns by Styling Library + +### Tailwind CSS — `tailwind.config.ts` + +Use `assets/theme-templates/tailwind.config.ts` as base. + +Key pattern: +```typescript +theme: { + extend: { + colors: { + brand: { + primary: tokens.primary, + 'primary-hover': tokens.primaryHover, + surface: tokens.surface, + background: tokens.background, + error: tokens.error, + } + }, + borderRadius: { + card: tokens.radiusLg, + input: tokens.radiusMd, + btn: tokens.radiusMd, + } + } +} +``` + +Usage in components: `bg-brand-primary`, `hover:bg-brand-primary-hover`, `rounded-card`. + +### CSS Modules — `styles/tokens.css` + +Use `assets/theme-templates/tokens.css` as base. + +Pattern: define all tokens as `:root` CSS custom properties. +```css +:root { + --color-primary: #4F46E5; + --color-primary-hover: #4338CA; + /* ... */ +} +``` + +Usage: `background: var(--color-primary)`. + +### styled-components — `theme/index.ts` + +Use `assets/theme-templates/theme-provider.ts` as base. + +Pattern: +```typescript +export const theme = { + colors: { primary: '#4F46E5', ... }, + radii: { card: '12px', ... } +} + +// Wrap app + +``` + +Usage in styled components: `background: ${({ theme }) => theme.colors.primary}`. + +### Plain CSS — `styles/globals.css` + +Use `assets/theme-templates/globals.css` as base. Same as CSS Modules pattern but applied globally. + +--- + +## Single Screen vs All Screens + +### Single screen (inline) +Apply tokens directly in the component's style file. No shared theme file. +```css +/* LoginId.module.css */ +.card { background: #FFFFFF; border-radius: 12px; } +.submitBtn { background: #4F46E5; } +``` + +### All screens (shared theme file) +1. Generate the shared theme file first (`tailwind.config.ts` / `tokens.css` / etc.) +2. All screen components import from that single source of truth +3. Consistency is enforced — changing one variable updates all screens + +**File to generate per styling library:** + +| Library | File to create | Import in components | +|---------|---------------|----------------------| +| Tailwind | `tailwind.config.ts` | Classes only (no import needed) | +| CSS Modules | `styles/tokens.css` | `@import '../styles/tokens.css'` | +| styled-components | `theme/index.ts` | `import { theme } from '../theme'` | +| Plain CSS | `styles/globals.css` | Import once in entry point | diff --git a/main/.mintlify/skills/auth0-android/SKILL.md b/main/.mintlify/skills/auth0-android/SKILL.md index 0048023133..c020b90da8 100644 --- a/main/.mintlify/skills/auth0-android/SKILL.md +++ b/main/.mintlify/skills/auth0-android/SKILL.md @@ -1,9 +1,17 @@ --- name: auth0-android description: Use when adding authentication to Android applications (Kotlin/Java) with Web Auth, biometric-protected credentials, and MFA - integrates com.auth0.android:auth0 SDK for native Android apps -license: Proprietary +license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills + requires: + bins: + - gh + - node --- # Auth0 Android Integration @@ -40,7 +48,14 @@ Add authentication to Android applications using `com.auth0.android:auth0`. 2. **Configure Auth0**: - See [**Setup Guide**](references/setup.md) for automatic/manual setup, post-setup required project changes, and callback URL configuration. + > **Agent instruction:** Check whether the user prompt already includes both Auth0 **Client ID** and **Domain**. + > - If both are provided, proceed directly to **Manual Setup** in [**Setup Guide**](references/setup.md) using those values. + > - If either is missing, you MUST ask the user BEFORE writing any code or files: + > - Question: "How would you like to configure Auth0 for this project?" + > - Options: "Automatic setup (Recommended) — Auth0 CLI creates the app and writes credentials to strings.xml" / "Manual setup — I'll provide my Client ID and Domain" + > + > Then follow [**Setup Guide**](references/setup.md) for the chosen path. + > **Do NOT proceed to step 3 until Auth0 credentials are confirmed.** 3. **Initialize**: Create an Auth0 account instance: ```kotlin @@ -117,11 +132,9 @@ Add authentication to Android applications using `com.auth0.android:auth0`. > > Re-run the build after each fix. Track the number of build-fix iterations. > - > **Failcheck:** If the build still fails after 5–6 fix attempts, stop and ask the user using `AskUserQuestion`: - > _"The build is still failing after several fix attempts. How would you like to proceed?"_ - > - **Let the skill continue fixing iteratively** — continue the build-fix loop for another 5–6 attempts - > - **Fix it manually** — show the remaining errors and let the user resolve them - > - **Skip build verification** — proceed without a successful build + > **Failcheck:** If the build still fails after 5–6 fix attempts, stop and ask the user: + > - Question: "The build is still failing after several fix attempts. How would you like to proceed?" + > - Options: "Let the agent continue fixing iteratively" / "I'll fix it manually — show me the errors" / "Skip build verification and proceed" > > Repeat this check after every 5–6 iterations if errors persist. Do not leave the project in a non-compiling state without the user's explicit consent. @@ -150,6 +163,7 @@ Add authentication to Android applications using `com.auth0.android:auth0`. - [auth0-quickstart](/auth0-quickstart) — Set up an Auth0 account and application - [auth0-mfa](/auth0-mfa) — Configure multi-factor authentication - [auth0-swift](/auth0-swift) — iOS/macOS authentication +- [auth0-cli](/auth0-cli) — Manage Auth0 resources from the terminal ## Quick Reference diff --git a/main/.mintlify/skills/auth0-android/references/api.md b/main/.mintlify/skills/auth0-android/references/api.md new file mode 100644 index 0000000000..641d4af55d --- /dev/null +++ b/main/.mintlify/skills/auth0-android/references/api.md @@ -0,0 +1,269 @@ +# Auth0 Android Testing & Reference + +## Testing Checklist + +Before deploying your Auth0 Android integration, verify: + +- [ ] **Emulator Testing** + - [ ] Login flow completes end-to-end + - [ ] Logout clears credentials + - [ ] Credentials persist after app restart + - [ ] Token refresh works when token expires + - [ ] Error messages display correctly + +- [ ] **Physical Device Testing** + - [ ] Login flow works on actual device + - [ ] Custom Tabs browser opens correctly + - [ ] Deep link callback works (https:// or custom scheme) + - [ ] Biometric authentication prompts appear (if implemented) + - [ ] App Links work with https:// scheme + +- [ ] **Auth0 Configuration** + - [ ] Callback URL matches exactly in Auth0 Dashboard + - [ ] Logout URL configured in Auth0 Dashboard + - [ ] Application type is "Native" (not SPA or Machine-to-Machine) + - [ ] Client ID and domain are correct + +- [ ] **Security** + - [ ] Credentials stored via SecureCredentialsManager + - [ ] No tokens logged to console + - [ ] INTERNET permission added to manifest + - [ ] ProGuard rules not stripping Auth0 classes + +- [ ] **Edge Cases** + - [ ] User cancels login mid-flow + - [ ] Network timeout during login + - [ ] Device goes to sleep during login + - [ ] Token refresh fails gracefully + - [ ] MFA challenges work (if enabled) + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Deep link not working after login | Callback URL mismatch or manifest placeholders not set | Verify callback URL format: `https://{DOMAIN}/android/{PACKAGE}/callback`. Ensure `auth0Domain` and `auth0Scheme` in manifest placeholders. Rebuild and reinstall app. | +| "Invalid state" error on redirect | Authentication session timed out or was invalidated | This can happen after long delays or device sleep. Redirect user to login again. For testing, keep login window active. | +| Custom Tabs browser not opening | No browser available on device or Custom Tabs disabled | Check `error.isBrowserAppNotAvailable`. Ensure device has Chrome or compatible browser. Fallback to system browser if needed. | +| Biometric prompt not showing | Min API < 21, biometric not enrolled, or options not set | Set min SDK to 21+. Enroll fingerprint/face on device. Verify `LocalAuthenticationOptions` and `BiometricPolicy` configuration. Check `setDeviceCredentialFallback(true)` for PIN/password fallback. | +| Token refresh fails and user can't access APIs | Refresh token expired (typically after 30 days of inactivity) | Catch `CredentialsManagerException` with code `REFRESH_FAILED`. Trigger `WebAuthProvider.login()` to re-authenticate. Inform user they need to log in again. | +| ProGuard/R8 strips Auth0 classes and crashes | ProGuard rules not applied | Auth0 rules are bundled automatically. If issues occur, add `-keep class com.auth0.** { *; }` to your `proguard-rules.pro` or disable minification for testing. | +| Login works on emulator but fails on physical device | Certificate pinning or network differences | Ensure device has valid time/date. Check network connectivity. For HTTPS scheme, verify Digital Asset Links are set up for your domain. | +| Credentials lost after app update | Shared storage encrypted with device key that changed | This is expected behavior after major system updates. Gracefully handle `NO_CREDENTIALS` and redirect to login. | + +## Security Considerations + +### PKCE Enabled by Default + +The Auth0 Android SDK automatically enables PKCE (Proof Key for Code Exchange) for all authorization flows. PKCE provides an extra layer of security for native apps and is always used by `WebAuthProvider`. + +### Secure Credential Storage + +Always use `SecureCredentialsManager` for token storage: + +```kotlin +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager(context, authentication, storage) +manager.saveCredentials(credentials) // Encrypted at rest +``` + +Never store tokens in: +- Plain `SharedPreferences` — Not encrypted +- `DataStore` without encryption — Unencrypted +- App-level files — Accessible to other apps + +### HTTPS Scheme Recommended + +Prefer `https://` scheme over custom schemes: + +```gradle +manifestPlaceholders = [ + auth0Domain: "@string/com_auth0_domain", + auth0Scheme: "@string/com_auth0_scheme" +] +``` + +Benefits: +- Leverages Android App Links for secure deep linking +- Requires Digital Asset Links verification +- No prompt when opening links +- More difficult to intercept + +Custom schemes are lower security but work if HTTPS is not feasible. + +### Never Log Tokens + +Do not log access tokens, ID tokens, or refresh tokens: + +```kotlin +// BAD +Log.d("Auth0", "Token: ${credentials.accessToken}") + +// GOOD +Log.d("Auth0", "Authentication successful") +``` + +Logs may be accessible via `adb logcat` or included in crash reports. + +### Validate Tokens + +Always call `.validateClaims()` when using direct API calls: + +```kotlin +authentication.login(...) + .validateClaims() // Validates ID token claims + .start(callback) +``` + +This verifies: +- Token signature +- Token expiration +- Audience (aud) claim +- Issuer (iss) claim + +`WebAuthProvider` validates automatically, but direct API calls do not. + +### Biometric Protection + +When storing credentials with biometric protection, use strong authentication: + +```kotlin +val options = LocalAuthenticationOptions.Builder() + .setAuthenticationLevel(AuthenticationLevel.STRONG) + .setPolicy(BiometricPolicy.Always) + .build() +``` + +Avoid: +- `AuthenticationLevel.WEAK` for sensitive operations +- `BiometricPolicy.Never` when protecting credentials +- `setDeviceCredentialFallback(true)` with WEAK level + +## API Reference + +### Auth0 + +Entry point for SDK initialization: + +```kotlin +// From strings.xml +val account = Auth0.getInstance(context) + +// Direct +val account = Auth0.getInstance("CLIENT_ID", "DOMAIN") +``` + +### WebAuthProvider + +Browser-based OAuth 2.0 flow: + +```kotlin +WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withScope("openid profile email") + .withAudience("https://api.example.com") + .withConnection("google-oauth2") + .withOrganization("org_id") + .withInvitation("invitation_id") + .withPrompt("login") // "login" or "none" + .withCustomTabsOptions(customTabs) + .start(context, callback) + +WebAuthProvider.logout(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .start(context, callback) +``` + +### AuthenticationAPIClient + +Direct API calls: + +```kotlin +val authentication = AuthenticationAPIClient(account) + +authentication.login(email, password, realm) +authentication.signUp(email, password, username, connection) +authentication.passwordlessWithEmail(email, type) +authentication.loginWithEmail(email, code) +authentication.mfaClient(mfaToken) +``` + +### SecureCredentialsManager + +Secure credential storage: + +```kotlin +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager(context, authentication, storage) + +manager.saveCredentials(credentials) +manager.hasValidCredentials(): Boolean +manager.getCredentials(callback) +manager.clearCredentials() +``` + +### Credentials + +User tokens and metadata: + +```kotlin +val accessToken = credentials.accessToken // OAuth 2.0 access token +val idToken = credentials.idToken // OpenID Connect ID token +val refreshToken = credentials.refreshToken // Refresh token +val expiresAt = credentials.expiresAt // Expiration timestamp +val scope = credentials.scope // Granted scopes +val type = credentials.type // "Bearer" + +// ID token claims +val sub = credentials.claims["sub"] // Subject (user ID) +val name = credentials.claims["name"] +val email = credentials.claims["email"] +val emailVerified = credentials.claims["email_verified"] +``` + +### LocalAuthenticationOptions + +Biometric authentication configuration: + +```kotlin +LocalAuthenticationOptions.Builder() + .setTitle("Authenticate") + .setDescription("Verify your identity") + .setAuthenticationLevel(AuthenticationLevel.STRONG) + .setNegativeButtonText("Cancel") + .setDeviceCredentialFallback(true) + .setPolicy(BiometricPolicy.Session(300)) + .build() +``` + +### Exception Handling + +```kotlin +// AuthenticationException +error.isMultifactorRequired: Boolean +error.isBrowserAppNotAvailable: Boolean +error.isAuthenticationCanceled: Boolean +error.statusCode: Int +error.message: String? + +// CredentialsManagerException +error.code: String // "NO_CREDENTIALS", "CREDENTIALS_EXPIRED", "REFRESH_FAILED" +error.message: String? +``` + +## Related Skills + +- [auth0-mfa](/auth0-mfa) — Configure multi-factor authentication +- [auth0-quickstart](/auth0-quickstart) — Set up an Auth0 account and application +- [auth0-swift](/auth0-swift) — iOS/macOS authentication +- [auth0-react-native](/auth0-react-native) — React Native authentication + +## References + +- [Auth0 Android SDK Documentation](https://auth0.com/docs/libraries/auth0-android) +- [Auth0 Android Quickstart](https://auth0.com/docs/quickstart/native/android) +- [Auth0 Android GitHub Repository](https://github.com/auth0/auth0-android) +- [Android SDK Javadoc](https://auth0.com/docs/references/android) +- [Sample App](https://github.com/auth0-samples/auth0-android-sample) +- [Android Security Best Practices](https://developer.android.com/privacy-and-security) diff --git a/main/.mintlify/skills/auth0-android/references/integration.md b/main/.mintlify/skills/auth0-android/references/integration.md new file mode 100644 index 0000000000..8377b24701 --- /dev/null +++ b/main/.mintlify/skills/auth0-android/references/integration.md @@ -0,0 +1,571 @@ +# Auth0 Android Integration Patterns + +> **Agent instruction:** Before creating new UI elements (buttons, click handlers), search the user's project for existing login/logout/sign-in/sign-out click handlers. If found, hook Auth0 code into the existing handlers without modifying the UI. Only create new buttons if no existing handlers are found. + +## Web Auth Login + +Use the browser-based Web Auth flow for the most secure login experience: + +```kotlin +import com.auth0.android.Auth0 +import com.auth0.android.provider.WebAuthProvider +import com.auth0.android.callback.Callback +import com.auth0.android.result.Credentials +import com.auth0.android.authentication.AuthenticationException + +val account = Auth0.getInstance(context) + +WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withScope("openid profile email offline_access") + .withAudience("https://api.example.com") // Optional: your API audience + .withOrganization("org_abc123") // Optional: for organization login + .start(context, object : Callback { + override fun onSuccess(result: Credentials) { + // User authenticated successfully + val idToken = result.idToken + val accessToken = result.accessToken + val refreshToken = result.refreshToken + val expiresAt = result.expiresAt + + // Store credentials securely (see Credential Storage section) + } + + override fun onFailure(error: AuthenticationException) { + // Handle authentication failure + when { + error.isBrowserAppNotAvailable -> { + // No browser available on device + } + error.isAuthenticationCanceled -> { + // User canceled the login + } + else -> { + // Other authentication error + Log.e("Auth0", error.message.orEmpty()) + } + } + } + }) +``` + +**Options**: +- `.withScheme()` — URL scheme matching `com_auth0_scheme` in strings.xml (required) +- `.withScope()` — Requested scopes (space-separated) +- `.withAudience()` — Your API identifier for the access token +- `.withOrganization()` — Organization ID or name for SSO +- `.withConnection()` — Force a specific connection (e.g., "google-oauth2") +- `.withPrompt()` — Force login prompt: `"login"` or `"none"` + +## Web Auth Logout + +Log out the user and clear their session: + +```kotlin +WebAuthProvider.logout(account) + .withScheme(getString(R.string.com_auth0_scheme)) // Match your configured scheme + .start(this, object : Callback { + override fun onSuccess(result: Void) { + // User logged out successfully + // Clear your app's local state + } + + override fun onFailure(error: AuthenticationException) { + // Logout failed + Log.e("Auth0", "Logout error: ${error.message}") + } + }) +``` + +After logout, clear stored credentials: + +```kotlin +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(this) +val manager = SecureCredentialsManager(this, authentication, storage) +manager.clearCredentials() +``` + +## Credential Storage + +Store and retrieve credentials securely using `SecureCredentialsManager`: + +```kotlin +import com.auth0.android.authentication.AuthenticationAPIClient +import com.auth0.android.authentication.storage.CredentialsManagerException +import com.auth0.android.authentication.storage.SecureCredentialsManager +import com.auth0.android.authentication.storage.SharedPreferencesStorage +import com.auth0.android.callback.Callback +import com.auth0.android.result.Credentials + +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager(context, authentication, storage) + +// Save credentials after login +manager.saveCredentials(credentials) + +// Check if valid credentials exist +if (manager.hasValidCredentials()) { + // Valid credentials stored +} + +// Retrieve credentials (auto-refreshes if needed) +manager.getCredentials(object : Callback { + override fun onSuccess(result: Credentials) { + val accessToken = result.accessToken + val idToken = result.idToken + // Use tokens for API calls + } + + override fun onFailure(error: CredentialsManagerException) { + when (error.code) { + "NO_CREDENTIALS" -> { + // No credentials stored + } + "CREDENTIALS_EXPIRED" -> { + // Credentials expired, user needs to login again + } + "REFRESH_FAILED" -> { + // Refresh token expired, trigger re-authentication + } + else -> Log.e("CredentialsManager", error.message.orEmpty()) + } + } +}) + +// Clear credentials (logout) +manager.clearCredentials() +``` + +**Key Features**: +- Credentials are encrypted at rest +- Automatic token refresh when credentials expire +- Handles refresh token expiration gracefully + +## Biometric-Protected Credentials + +Protect stored credentials with biometric authentication: + +```kotlin +import com.auth0.android.authentication.AuthenticationAPIClient +import com.auth0.android.authentication.storage.SecureCredentialsManager +import com.auth0.android.authentication.storage.SharedPreferencesStorage +import com.auth0.android.authentication.storage.LocalAuthenticationOptions +import com.auth0.android.authentication.storage.AuthenticationLevel +import com.auth0.android.authentication.storage.BiometricPolicy +import androidx.fragment.app.FragmentActivity + +val localAuthOptions = LocalAuthenticationOptions.Builder() + .setTitle("Authenticate") + .setDescription("Verify your fingerprint to access your account") + .setAuthenticationLevel(AuthenticationLevel.STRONG) // Fingerprint or face recognition + .setNegativeButtonText("Cancel") + .setDeviceCredentialFallback(true) // Allow PIN/password fallback + .setPolicy(BiometricPolicy.Session(300)) // Require biometric every 5 minutes + .build() + +val fragmentActivity: FragmentActivity = this // Your Activity +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager( + context, + authentication, + storage, + fragmentActivity, + localAuthOptions +) + +// Credentials are now biometric-protected +manager.saveCredentials(credentials) + +// User must authenticate with biometric/device credential to retrieve +manager.getCredentials(callback) +``` + +**Authentication Levels**: +- `AuthenticationLevel.STRONG` — Biometric authentication required +- `AuthenticationLevel.WEAK` — Biometric or device credential (PIN/password) +- `AuthenticationLevel.DEVICE_CREDENTIAL` — PIN/password only + +**Biometric Policies**: +- `BiometricPolicy.Never` — Never require biometric for retrieval +- `BiometricPolicy.Always` — Always require biometric +- `BiometricPolicy.Session(seconds)` — Require biometric every N seconds +- `BiometricPolicy.AppLifecycle` — Require biometric on app resume + +## Database Login + +Authenticate using username and password (requires `.validateClaims()`): + +```kotlin +import com.auth0.android.authentication.AuthenticationAPIClient +import com.auth0.android.callback.Callback +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.result.Credentials + +val authentication = AuthenticationAPIClient(account) + +authentication.login( + email = "user@example.com", + password = "securePassword123", + realm = "Username-Password-Authentication" +) + .validateClaims() // Critical: validate ID token claims + .setScope("openid profile email offline_access") + .start(object : Callback { + override fun onSuccess(result: Credentials) { + // User authenticated + manager.saveCredentials(result) + } + + override fun onFailure(error: AuthenticationException) { + when { + error.isMultifactorRequired -> { + // MFA required - see MFA Handling section + } + error.statusCode == 403 -> { + // Invalid credentials + } + else -> Log.e("Auth0", error.message.orEmpty()) + } + } + }) +``` + +**Important**: Always call `.validateClaims()` when using `AuthenticationAPIClient` directly. + +## Passwordless Authentication + +Two-step passwordless flow using email codes: + +### Step 1: Request Passwordless Code + +```kotlin +import com.auth0.android.authentication.AuthenticationAPIClient +import com.auth0.android.authentication.PasswordlessType +import com.auth0.android.callback.Callback +import com.auth0.android.authentication.AuthenticationException + +val authentication = AuthenticationAPIClient(account) + +authentication.passwordlessWithEmail( + email = "user@example.com", + type = PasswordlessType.CODE +) + .start(object : Callback { + override fun onSuccess(result: Void?) { + // Code sent to email - show user a screen to enter code + } + + override fun onFailure(error: AuthenticationException) { + Log.e("Auth0", error.message.orEmpty()) + } + }) +``` + +### Step 2: Log In with Code + +```kotlin +authentication.loginWithEmail( + email = "user@example.com", + code = "123456" // Code from email +) + .validateClaims() + .start(object : Callback { + override fun onSuccess(result: Credentials) { + // User authenticated + manager.saveCredentials(result) + } + + override fun onFailure(error: AuthenticationException) { + // Invalid or expired code + Log.e("Auth0", error.message.orEmpty()) + } + }) +``` + +## Sign Up + +Create a new account using the database connection: + +```kotlin +val authentication = AuthenticationAPIClient(account) + +authentication.signUp( + email = "newuser@example.com", + password = "securePassword123", + username = "newuser", + connection = "Username-Password-Authentication" +) + .start(object : Callback { + override fun onSuccess(result: Void?) { + // Account created successfully - user should now log in + } + + override fun onFailure(error: AuthenticationException) { + when { + error.statusCode == 400 -> { + // User already exists or validation error + } + else -> Log.e("Auth0", error.message.orEmpty()) + } + } + }) +``` + +After successful sign up, direct the user to log in using the database login flow. + +## Calling Protected APIs + +Attach the access token to your API requests: + +```kotlin +import com.auth0.android.authentication.AuthenticationAPIClient +import com.auth0.android.authentication.storage.CredentialsManagerException +import com.auth0.android.authentication.storage.SecureCredentialsManager +import com.auth0.android.authentication.storage.SharedPreferencesStorage +import com.auth0.android.callback.Callback +import com.auth0.android.result.Credentials +import okhttp3.OkHttpClient +import okhttp3.Interceptor + +val authentication = AuthenticationAPIClient(account) +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager(context, authentication, storage) + +manager.getCredentials(object : Callback { + override fun onSuccess(result: Credentials) { + val accessToken = result.accessToken + + // Use with OkHttp + val httpClient = OkHttpClient.Builder() + .addInterceptor(Interceptor { chain -> + val request = chain.request().newBuilder() + .header("Authorization", "Bearer $accessToken") + .build() + chain.proceed(request) + }) + .build() + + // Or manually for other HTTP libraries + val headers = mapOf("Authorization" to "Bearer $accessToken") + // Use headers in your API request + } + + override fun onFailure(error: CredentialsManagerException) { + // Handle error - may need to re-authenticate + } +}) +``` + +If the API returns 401 Unauthorized, refresh the credentials and retry: + +```kotlin +manager.getCredentials(object : Callback { + override fun onSuccess(result: Credentials) { + // Credentials were auto-refreshed by the manager + val newAccessToken = result.accessToken + retryApiCall(newAccessToken) + } + + override fun onFailure(error: CredentialsManagerException) { + if (error.code == "REFRESH_FAILED") { + // Refresh token expired - trigger login again + } + } +}) +``` + +## MFA Handling + +Handle multi-factor authentication challenges: + +### Detect MFA Required + +```kotlin +authentication.login(...) + .validateClaims() + .start(object : Callback { + override fun onFailure(error: AuthenticationException) { + if (error.isMultifactorRequired) { + val mfaToken = error.mfaRequiredErrorPayload?.mfaToken + // Proceed to enrollment or challenge screen + } + } + }) +``` + +### Enroll in MFA + +```kotlin +val mfaToken = error.mfaRequiredErrorPayload?.mfaToken ?: return +val mfaClient = authentication.mfaClient(mfaToken) + +// Enroll in OTP +mfaClient.enroll(MfaEnrollmentType.Otp) + .start(object : Callback { + override fun onSuccess(enrollment: MfaEnrollment) { + val recoveryCode = enrollment.recoveryCode + val secret = enrollment.secret // For OTP app + // Show QR code to user + } + + override fun onFailure(error: AuthenticationException) { + Log.e("MFA", error.message.orEmpty()) + } + }) +``` + +### Challenge MFA + +```kotlin +mfaClient.challenge( + authenticatorId = "dev_abc123", // From enrollments list + challengeType = MfaChallengeType.OTP +) + .start(object : Callback { + override fun onSuccess(challenge: MfaChallenge) { + val challengeId = challenge.challengeId + // Show user OTP input screen + } + + override fun onFailure(error: AuthenticationException) { + Log.e("MFA", error.message.orEmpty()) + } + }) +``` + +### Verify Challenge + +```kotlin +mfaClient.verifyChallenge( + challengeId = "Fe26...session_id", + otp = "123456" // User's one-time password +) + .validateClaims() + .start(object : Callback { + override fun onSuccess(result: Credentials) { + // MFA verified - user now authenticated + manager.saveCredentials(result) + } + + override fun onFailure(error: AuthenticationException) { + // Invalid OTP or expired challenge + Log.e("MFA", error.message.orEmpty()) + } + }) +``` + +## Organizations + +Use Organizations for enterprise SSO and multi-tenancy: + +```kotlin +// Log in with organization +WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withOrganization("org_abc123") // Organization ID + .withScope("openid profile email") + .start(this, object : Callback { + override fun onSuccess(result: Credentials) { + // User authenticated to organization + val orgId = result.claims["org_id"] + } + + override fun onFailure(error: AuthenticationException) { + // Handle error + } + }) + +// Handle organization invitation link +val uri = intent.data // From deep link +val organizationId = uri?.getQueryParameter("organization") +val invitation = uri?.getQueryParameter("invitation") + +if (invitation != null) { + WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withInvitation(invitation) + .start(this, callback) +} +``` + +## Error Handling + +Handle authentication errors gracefully: + +```kotlin +authentication.login(...) + .start(object : Callback { + override fun onFailure(error: AuthenticationException) { + when { + error.isMultifactorRequired -> { + // MFA enrollment or challenge required + } + error.isBrowserAppNotAvailable -> { + // No browser available + // Fallback to in-app WebView (not recommended) + } + error.isAuthenticationCanceled -> { + // User canceled the login flow + } + error.statusCode == 429 -> { + // Rate limited - too many login attempts + } + error.statusCode == 403 -> { + // Invalid credentials or user blocked + } + error.statusCode == 500 -> { + // Server error - retry later + } + else -> { + // Generic error + Log.e("Auth0", "Error: ${error.message}") + } + } + } + }) +``` + +**CredentialsManagerException codes**: +- `NO_CREDENTIALS` — No credentials stored +- `CREDENTIALS_EXPIRED` — Stored credentials expired +- `REFRESH_FAILED` — Refresh token expired or invalid +- `INVALID_SECURITY` — Biometric authentication failed + +## Custom Tabs + +Customize the browser appearance: + +```kotlin +import com.auth0.android.provider.CustomTabsOptions +import com.auth0.android.provider.WebAuthProvider + +val customTabs = CustomTabsOptions.newBuilder() + .withToolbarColor(R.color.toolbar_blue) + .withShowTitle(true) + .build() + +WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withCustomTabsOptions(customTabs) + .start(this, callback) +``` + +**Options**: +- `.withToolbarColor()` — Toolbar color resource +- `.withShowTitle()` — Show the page title +- `.withStartAnimations()` — Entrance animation +- `.withExitAnimations()` — Exit animation + +## Common Issues + +| Issue | Solution | +|-------|----------| +| Deep link callback not working | Verify callback URL matches exactly: `https://{DOMAIN}/android/{PACKAGE}/callback`. Check manifest placeholders in `build.gradle`. | +| "Invalid state" error on callback | The auth session timed out or was invalidated. This can happen if the device went to sleep. Redirect user to login again. | +| Custom Tabs not opening | User may have disabled Custom Tabs. The SDK falls back to Chrome or system browser. If no browser available, `isBrowserAppNotAvailable` is true. | +| Biometric prompt not showing | Min SDK must be 21+ for biometric. Device must have fingerprint/face sensor registered. `setDeviceCredentialFallback(true)` allows PIN/password. | +| Token refresh fails | Refresh token may have expired (typically after 30 days). Trigger re-authentication with `WebAuthProvider.login()`. | +| ProGuard obfuscation breaks Auth0 | Auth0 rules are included automatically. If issues occur, add `-keep class com.auth0.** { *; }` to your `proguard-rules.pro`. | diff --git a/main/.mintlify/skills/auth0-android/references/setup.md b/main/.mintlify/skills/auth0-android/references/setup.md new file mode 100644 index 0000000000..693b602f9b --- /dev/null +++ b/main/.mintlify/skills/auth0-android/references/setup.md @@ -0,0 +1,317 @@ +# Auth0 Android Setup Guide + +> **Agent instruction:** Before providing version numbers, fetch the latest release: +> `gh api repos/auth0/Auth0.Android/releases/latest --jq '.tag_name'` +> Replace `{LATEST_VERSION}` in all dependency lines with the result. + +## Setup Overview + +1. Add SDK dependency to `build.gradle` +2. Configure Auth0 (automatic inline script or manual credentials) +3. Add manifest placeholders and INTERNET permission (post-setup) + +## Auth0 Configuration + +> **Agent instruction:** First, check whether the user prompt already includes both Auth0 **Client ID** and **Domain**. +> - If both are provided, skip the setup-choice question and proceed directly to **Manual Setup (User-Provided Credentials)** using those values. +> - If either value is missing, ask the user: +> - Question: "How would you like to configure Auth0 for this project?" +> - Options: "Automatic setup (Recommended) — Auth0 CLI creates the app and writes credentials to strings.xml" / "Manual setup — I'll provide my Client ID and Domain" +> +> Follow the matching section below based on their choice. + +### Automatic Setup + +Below automates the setup. Inform the user that Auth0 credentials will be written to `strings.xml`. + +**Before running any part of this setup that writes to `strings.xml`, you MUST ask the user for explicit confirmation.** Follow the steps below precisely. + +#### Step 1: Check for existing strings.xml and confirm with user + +Before writing credentials, check whether a `strings.xml` already exists: + +```bash +test -f app/src/main/res/values/strings.xml && echo "STRINGS_EXISTS" || echo "STRINGS_NOT_FOUND" +``` + +Then ask the user for explicit confirmation before proceeding — do not continue until the user confirms: + +- If `strings.xml` exists, ask: + - Question: "A `strings.xml` file already exists. This setup will add or update the Auth0 credential entries (`com_auth0_client_id`, `com_auth0_domain`, `com_auth0_scheme`) without modifying other entries. Do you want to proceed?" + - Options: "Yes, update existing strings.xml" / "No, I'll update it manually" + +- If `strings.xml` does **not** exist, ask: + - Question: "This setup will create `app/src/main/res/values/strings.xml` with Auth0 credentials (`com_auth0_client_id`, `com_auth0_domain`, `com_auth0_scheme`). Do you want to proceed?" + - Options: "Yes, create strings.xml" / "No, I'll configure it manually" + +**Do not proceed with writing to strings.xml unless the user selects the confirmation option.** + +#### Step 2: Run automated setup (only after confirmation) + +```bash +#!/bin/bash + +PROJECT_PATH="${1:-$PWD}" +SCHEME="demo" + +# Install Auth0 CLI +if ! command -v auth0 &> /dev/null; then + [[ "$OSTYPE" == "darwin"* ]] && brew install auth0/auth0-cli/auth0 || \ + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh -s -- -b /usr/local/bin +fi + +# Login +auth0 login 2>/dev/null || auth0 login + +# Find build.gradle / build.gradle.kts +if [ -f "$PROJECT_PATH/app/build.gradle" ]; then + GRADLE_FILE="$PROJECT_PATH/app/build.gradle" +elif [ -f "$PROJECT_PATH/app/build.gradle.kts" ]; then + GRADLE_FILE="$PROJECT_PATH/app/build.gradle.kts" +else + echo "❌ No app/build.gradle or app/build.gradle.kts found in $PROJECT_PATH" + exit 1 +fi + +# Extract applicationId +PACKAGE_NAME=$(grep -E 'applicationId\s*=?\s*"[^"]*"' "$GRADLE_FILE" | grep -oE '"[^"]*"' | tr -d '"' | head -1) +if [ -z "$PACKAGE_NAME" ]; then + echo "❌ Could not find applicationId in $GRADLE_FILE" + exit 1 +fi + +# List existing apps and prompt to pick or create +auth0 apps list +read -p "Enter app ID (or press Enter to create a new one): " APP_ID + +if [ -z "$APP_ID" ]; then + DOMAIN=$(auth0 tenants list --csv --no-input 2>/dev/null | grep '→' | cut -d',' -f2 | tr -d ' ') + CALLBACK_URL="${SCHEME}://${DOMAIN}/android/${PACKAGE_NAME}/callback" + CLIENT_JSON=$(auth0 apps create \ + --name "${PACKAGE_NAME}-android" \ + --type native \ + --auth-method none \ + --callbacks "$CALLBACK_URL" \ + --logout-urls "$CALLBACK_URL" \ + --json \ + --no-input) + CLIENT_ID=$(echo "$CLIENT_JSON" | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) +else + CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) + CALLBACK_URL="${SCHEME}://${DOMAIN}/android/${PACKAGE_NAME}/callback" +fi + +# Check / create database connection +CONNECTIONS_JSON=$(auth0 api get connections --no-input 2>/dev/null || echo "[]") +CONNECTION_ID=$(echo "$CONNECTIONS_JSON" | python3 -c " +import sys, json +data = json.load(sys.stdin) +for c in data: + if c.get('name') == 'Username-Password-Authentication': + print(c['id']) + break +" 2>/dev/null) +ENABLED_CLIENTS=$(echo "$CONNECTIONS_JSON" | python3 -c " +import sys, json +data = json.load(sys.stdin) +for c in data: + if c.get('name') == 'Username-Password-Authentication': + print(json.dumps(c.get('enabled_clients', []))) + break +" 2>/dev/null) + +if [ -z "$CONNECTION_ID" ]; then + auth0 api post connections \ + --data "{\"strategy\":\"auth0\",\"name\":\"Username-Password-Authentication\",\"enabled_clients\":[\"$CLIENT_ID\"]}" \ + --no-input > /dev/null +else + UPDATED_CLIENTS=$(echo "$ENABLED_CLIENTS" | python3 -c " +import sys, json +clients = json.load(sys.stdin) +if '$CLIENT_ID' not in clients: + clients.append('$CLIENT_ID') +print(json.dumps(clients)) +") + auth0 api patch "connections/$CONNECTION_ID" \ + --data "{\"enabled_clients\":$UPDATED_CLIENTS}" \ + --no-input > /dev/null +fi + +# Write / update strings.xml +STRINGS_FILE="$PROJECT_PATH/app/src/main/res/values/strings.xml" +mkdir -p "$(dirname "$STRINGS_FILE")" + +python3 << PYEOF +import re, os + +path = "$STRINGS_FILE" +entries = { + 'com_auth0_client_id': '$CLIENT_ID', + 'com_auth0_domain': '$DOMAIN', + 'com_auth0_scheme': '$SCHEME', +} + +content = open(path).read() if os.path.exists(path) else '' + +if ']*>[\s\S]*?') + replacement = f'{value}' + if pattern.search(content): + content = pattern.sub(replacement, content) + else: + content = content.replace('', f' {value}\n') +else: + lines = [' My App'] + lines += [f' {v}' for k, v in entries.items()] + content = '\n\n' + '\n'.join(lines) + '\n\n' + +with open(path, 'w') as f: + f.write(content) +PYEOF + +echo "✅ Auth0 credentials written to $STRINGS_FILE" +echo " Domain: $DOMAIN" +echo " Client ID: $CLIENT_ID" +echo " Package: $PACKAGE_NAME" +echo " Callback URL: $CALLBACK_URL" +``` + +After the script runs, proceed to **Post-Setup Steps** below. + +### Manual Setup (User-Provided Credentials) + +> **Agent instruction:** Ask the user for their Auth0 **Client ID** and **Domain**. Then update `strings.xml` with the values they provide: +> ```xml +> USER_PROVIDED_CLIENT_ID +> USER_PROVIDED_DOMAIN +> demo +> ``` +> Remind the user to configure callback URLs in the Auth0 Dashboard: +> `demo://{DOMAIN}/android/{APPLICATION_ID}/callback` +> (add to both **Allowed Callback URLs** and **Allowed Logout URLs**). +> +> After updating strings.xml, proceed to **Post-Setup Steps** below. + +### Post-Setup Steps (Required for Both Paths) + +> **Agent instruction:** After either automatic or manual Auth0 configuration, the agent MUST apply the following changes to the project: +> +> 1. **Add manifest placeholders** to `app/build.gradle` (or `app/build.gradle.kts`) inside the `defaultConfig` block, if not already present: +> - Groovy (`build.gradle`): +> ```gradle +> manifestPlaceholders = [ +> auth0Domain: "@string/com_auth0_domain", +> auth0Scheme: "@string/com_auth0_scheme" +> ] +> ``` +> - Kotlin DSL (`build.gradle.kts`): +> ```kotlin +> manifestPlaceholders += mapOf( +> "auth0Domain" to "@string/com_auth0_domain", +> "auth0Scheme" to "@string/com_auth0_scheme" +> ) +> ``` +> +> 2. **Add INTERNET permission** to `AndroidManifest.xml` if not already present: +> ```xml +> +> ``` +> +> 3. **Build the project** to confirm everything compiles: +> ```bash +> ./gradlew assembleDebug +> ``` + +## SDK Installation + +Add the dependency to your module's `build.gradle`: + +```gradle +dependencies { + implementation 'com.auth0.android:auth0:{LATEST_VERSION}' +} +``` + +Ensure Java 8 compatibility in your `build.gradle`: + +```gradle +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} +``` + +## Android App Links (Recommended for Production) + +> **Note:** The automatic setup script and manual setup default to a custom scheme (`demo://`) for simplicity. App Links with `https://` are recommended for production apps. To switch, update `com_auth0_scheme` to `https` in `strings.xml` and update your callback URL in the Auth0 Dashboard to `https://YOUR_AUTH0_DOMAIN/android/YOUR_APP_PACKAGE_NAME/callback`. + +For the `https://` scheme, Android uses App Links for deeper integration: + +1. **Digital Asset Links**: Create a `assetlinks.json` file on your Auth0 domain + - Auth0 manages this automatically for you + - Enables deep link routing without user prompts + +2. **Auto-Verify**: Add to `build.gradle`: + ```gradle + android { + defaultConfig { + // The android:autoVerify attribute is added automatically for https schemes + } + } + ``` + +The SDK automatically uses App Links when `com_auth0_scheme` is set to `https` in `strings.xml`. + +## Custom Scheme (Alternative) + +If you need a custom scheme instead of `https://`: + +1. Update `strings.xml` with your custom scheme: + ```xml + myapp + ``` + + The manifest placeholder already references this via `@string/com_auth0_scheme`. + +2. Update callback URL in Auth0 Dashboard: + ``` + myapp://YOUR_AUTH0_DOMAIN/android/YOUR_APP_PACKAGE_NAME/callback + ``` + +3. In your code when logging out, use the same scheme: + ```kotlin + WebAuthProvider.logout(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .start(this, callback) + ``` + +**Important**: Android requires scheme names to be lowercase. + +## ProGuard/R8 + +The Auth0 Android SDK includes ProGuard/R8 rules automatically. You don't need to add any manual configuration. The library's `proguard-rules.pro` is included in the AAR file and will be merged into your app's build. + +If you encounter obfuscation issues: + +1. Disable obfuscation for Auth0 classes (in `proguard-rules.pro`): + ``` + -keep class com.auth0.** { *; } + ``` + +2. Or rebuild with debugging enabled temporarily: + ```gradle + buildTypes { + debug { + debuggable true + minifyEnabled false + } + } + ``` diff --git a/main/.mintlify/skills/auth0-angular/SKILL.md b/main/.mintlify/skills/auth0-angular/SKILL.md index 6d71fcdea1..6d17bfa09a 100644 --- a/main/.mintlify/skills/auth0-angular/SKILL.md +++ b/main/.mintlify/skills/auth0-angular/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding authentication to Angular applications with route g license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Angular Integration @@ -166,7 +170,7 @@ ng serve | Not configuring AuthModule properly | Must call `AuthModule.forRoot()` in NgModule or `provideAuth0()` in standalone config | | Accessing auth before initialization | Use `isLoading$` observable to wait for SDK initialization | | Storing tokens manually | Never manually store tokens - SDK handles secure storage automatically | -| Missing HTTP interceptor | Use `authHttpInterceptorFn` or `AuthHttpInterceptor` to attach tokens to API calls | +| No token sent to API | Use either `authHttpInterceptorFn` for automatic token attachment, or `getAccessTokenSilently()` for manual control — see [Integration Guide](references/integration.md#calling-a-protected-api) | | Route guard not protecting routes | Apply `AuthGuard` (or `authGuardFn`) to protected routes in routing config | --- @@ -176,6 +180,7 @@ ng serve - `auth0-quickstart` - Basic Auth0 setup - `auth0-migration` - Migrate from another auth provider - `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-cli` - Manage Auth0 resources from the terminal --- @@ -187,12 +192,12 @@ ng serve - `user$` - Observable user profile information - `loginWithRedirect()` - Initiate login - `logout()` - Log out user -- `getAccessTokenSilently()` - Get access token for API calls +- `getAccessTokenSilently()` - Get access token manually (alternative to HTTP interceptor) **Common Use Cases:** - Login/Logout buttons → See Step 4 above - Protected routes with guards → [Integration Guide](references/integration.md#protected-routes) -- HTTP interceptors for API calls → [Integration Guide](references/integration.md#http-interceptor) +- Calling a protected API → [Integration Guide](references/integration.md#calling-a-protected-api) - Error handling → [Integration Guide](references/integration.md#error-handling) --- diff --git a/main/.mintlify/skills/auth0-angular/references/api.md b/main/.mintlify/skills/auth0-angular/references/api.md new file mode 100644 index 0000000000..b3975042e8 --- /dev/null +++ b/main/.mintlify/skills/auth0-angular/references/api.md @@ -0,0 +1,321 @@ +## Common Patterns + +### Protected Route with Auth Guard + +Create an auth guard (`src/app/auth.guard.ts`): + +```typescript +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '@auth0/auth0-angular'; +import { map, take } from 'rxjs/operators'; + +export const authGuard = () => { + const auth = inject(AuthService); + const router = inject(Router); + + return auth.isAuthenticated$.pipe( + take(1), + map(isAuthenticated => { + if (!isAuthenticated) { + auth.loginWithRedirect(); + return false; + } + return true; + }) + ); +}; +``` + +**Configure routes** (`src/app/app.routes.ts`): + +```typescript +import { Routes } from '@angular/router'; +import { authGuard } from './auth.guard'; +import { HomeComponent } from './home/home.component'; +import { ProfileComponent } from './profile/profile.component'; + +export const routes: Routes = [ + { path: '', component: HomeComponent }, + { + path: 'profile', + component: ProfileComponent, + canActivate: [authGuard] // Protect this route + } +]; +``` + +--- + +### Get User Profile Component + +Create `src/app/profile/profile.component.ts`: + +```typescript +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-profile', + standalone: true, + imports: [CommonModule], + template: ` +
+

Profile

+ +

Name: {{ user.name }}

+

Email: {{ user.email }}

+

User ID: {{ user.sub }}

+
+ ` +}) +export class ProfileComponent { + constructor(public auth: AuthService) {} +} +``` + +--- + +### Call Protected API (Manual Token Approach) + +This example uses `getAccessTokenSilently()` to manually obtain and attach tokens. This is an alternative to using the built-in HTTP interceptor — see the [Integration Guide](integration.md#calling-a-protected-api) for both approaches. + +Create `src/app/api-test/api-test.component.ts`: + +```typescript +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; +import { HttpClient } from '@angular/common/http'; +import { switchMap } from 'rxjs/operators'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-api-test', + standalone: true, + imports: [CommonModule], + template: ` +
+ +
Error: {{ error }}
+
{{ data | json }}
+
+ ` +}) +export class ApiTestComponent { + data: any = null; + error: string | null = null; + + constructor( + private auth: AuthService, + private http: HttpClient + ) {} + + callApi(): void { + this.auth.getAccessTokenSilently({ + authorizationParams: { + audience: 'https://your-api-identifier' + } + }).pipe( + switchMap(token => + this.http.get('https://your-api.com/data', { + headers: { + Authorization: `Bearer ${token}` + } + }) + ) + ).subscribe({ + next: (response) => { + this.data = response; + }, + error: (err) => { + this.error = err.message; + } + }); + } +} +``` + +**Note:** If calling APIs, add `audience` to your Auth module configuration: + +```typescript +AuthModule.forRoot({ + domain: environment.auth0.domain, + clientId: environment.auth0.clientId, + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://your-api-identifier' // Add this + } +}) +``` + +--- + +### Custom HTTP Interceptor for API Calls + +This shows how to build a custom interceptor from scratch. In most cases, you should use the SDK's built-in `authHttpInterceptorFn` instead — see the [Integration Guide](integration.md#calling-a-protected-api). A custom interceptor is only needed when you require logic beyond what `allowedList` provides. + +Create `src/app/auth.interceptor.ts`: + +```typescript +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; +import { switchMap } from 'rxjs/operators'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const auth = inject(AuthService); + + // Only add token to API calls + if (req.url.startsWith('https://your-api.com')) { + return auth.getAccessTokenSilently().pipe( + switchMap(token => { + const clonedReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + return next(clonedReq); + }) + ); + } + + return next(req); +}; +``` + +**Register interceptor** (`src/app/app.config.ts`): + +```typescript +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authInterceptor } from './auth.interceptor'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAuth0({...}), + provideHttpClient( + withInterceptors([authInterceptor]) + ) + ] +}; +``` + +--- + +## Configuration Options + +### Complete Auth Configuration + +```typescript +AuthModule.forRoot({ + domain: 'your-tenant.auth0.com', + clientId: 'your-client-id', + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://your-api-identifier', // For API calls + scope: 'openid profile email', // Default scopes + }, + cacheLocation: 'localstorage', // or 'memory' + useRefreshTokens: true, // Enable refresh tokens + skipRedirectCallback: false, // Skip automatic callback handling + errorPath: '/error', // Path to redirect on auth errors (default: '/') + httpInterceptor: { + allowedList: [ + 'https://your-api.com/*' // Automatically add tokens to these URLs + ] + } +}) +``` + +--- + +## Testing + +1. Start your dev server: `ng serve` +2. Navigate to `http://localhost:4200` +3. Click "Login" button +4. Complete Auth0 Universal Login +5. Verify redirect back with user authenticated +6. Test protected routes +7. Click "Logout" and verify user is logged out + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Invalid state" error | Clear browser storage. Ensure `redirect_uri` matches configured callback URL | +| User stuck on loading | Check Auth0 application has `http://localhost:4200` in callback URLs | +| API calls fail with 401 | Ensure `audience` is configured and matches your API identifier | +| Logout doesn't work | Include `returnTo` URL and configure in Auth0 "Allowed Logout URLs" | +| HTTP interceptor not working | Check `allowedList` includes your API URLs | + +--- + +## Security Considerations + +- **Never expose client secret** - Angular is client-side, use only public client credentials +- **Use PKCE** - Enabled by default with @auth0/auth0-angular +- **Validate tokens on backend** - Never trust client-side token validation +- **Use HTTPS in production** - Auth0 requires HTTPS for production redirect URLs +- **Implement proper CORS** - Configure allowed origins in Auth0 application settings + +--- + +## Advanced Methods + +### getAccessTokenWithPopup + +Gets an access token via popup window. Useful when silent authentication fails (e.g., third-party cookies blocked). + +```typescript +// Try silent, fall back to popup +this.auth.getAccessTokenSilently().subscribe({ + next: (token) => { + // Use the token (e.g., attach to API requests) + }, + error: () => { + // Silent auth failed, try popup + this.auth.getAccessTokenWithPopup().subscribe(token => { + // Use the token (e.g., attach to API requests) + }); + } +}); +``` + +### connectAccountWithRedirect + +Redirects to connect an additional account to the logged-in user. Allows users to link multiple identity providers. + +```typescript +// Link a Google account to existing user +this.auth.connectAccountWithRedirect({ + connection: 'google-oauth2', + scopes: ['openid', 'profile', 'email'], + authorizationParams: { + // additional params + } +}).subscribe(); +``` + +After the redirect callback, `handleRedirectCallback` will be called with the details of the connected account. + +--- + +## Related Skills + +- `auth0-quickstart` - Initial Auth0 account setup +- `auth0-migration` - Migrate from another auth provider +- `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-organizations` - B2B multi-tenancy support +- `auth0-passkeys` - Add passkey authentication + +--- + +## References + +- [Auth0 Angular SDK Documentation](https://auth0.com/docs/libraries/auth0-angular) +- [Auth0 Angular SDK GitHub](https://github.com/auth0/auth0-angular) +- [Auth0 Angular Quickstart](https://auth0.com/docs/quickstart/spa/angular) +- [Angular Router Documentation](https://angular.io/guide/router) diff --git a/main/.mintlify/skills/auth0-angular/references/integration.md b/main/.mintlify/skills/auth0-angular/references/integration.md new file mode 100644 index 0000000000..23b391493c --- /dev/null +++ b/main/.mintlify/skills/auth0-angular/references/integration.md @@ -0,0 +1,278 @@ +# Auth0 Angular Integration Patterns + +Angular-specific implementation patterns with route guards, HTTP interceptors, and RxJS. + +--- + +## Protected Routes + +### Auth Guard + +Create `src/app/guards/auth.guard.ts`: + +```typescript +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '@auth0/auth0-angular'; +import { map } from 'rxjs/operators'; + +export const authGuard = () => { + const authService = inject(AuthService); + const router = inject(Router); + + return authService.isAuthenticated$.pipe( + map(isAuthenticated => { + if (!isAuthenticated) { + authService.loginWithRedirect(); + return false; + } + return true; + }) + ); +}; +``` + +### Apply Guard to Routes + +```typescript +// app.routes.ts (standalone) +import { Routes } from '@angular/router'; +import { authGuard } from './guards/auth.guard'; + +export const routes: Routes = [ + { path: '', component: HomeComponent }, + { + path: 'profile', + component: ProfileComponent, + canActivate: [authGuard] + } +]; +``` + +--- + +## Calling a Protected API + +There are two alternative approaches to attach access tokens to API requests. Choose the one that best fits your needs — you do not need both: + +- **HTTP Interceptor (recommended)** — Automatically attaches tokens to outgoing requests matching a configured URL list. This is the simplest, most centralized approach and works well for most applications. +- **Manual token retrieval** — Call `getAccessTokenSilently()` to obtain a token and attach it to requests yourself. Use this when you need explicit, per-request control over token handling. + +### Option 1: HTTP Interceptor + +Configure the built-in HTTP interceptor in app config: + +```typescript +// app.config.ts +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authHttpInterceptorFn } from '@auth0/auth0-angular'; +import { environment } from '../environments/environment'; // Adjust path as needed + +export const appConfig: ApplicationConfig = { + providers: [ + provideAuth0({ + domain: environment.auth0.domain, + clientId: environment.auth0.clientId, + authorizationParams: { + audience: 'https://your-api-identifier', + redirect_uri: window.location.origin + }, + httpInterceptor: { + allowedList: [ + '/api/*', + 'https://your-api.com/*' + ] + } + }), + provideHttpClient( + withInterceptors([authHttpInterceptorFn]) + ) + ] +}; +``` + +With this in place, any `HttpClient` request to a URL matching `allowedList` will automatically include the access token: + +```typescript +// data.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ providedIn: 'root' }) +export class DataService { + constructor(private http: HttpClient) {} + + getData() { + return this.http.get('https://your-api.com/data'); + // Access token automatically added by interceptor + } +} +``` + +### Option 2: Manual Token Retrieval + +If you prefer explicit control instead of using the interceptor, call `getAccessTokenSilently()` to obtain a token and attach it yourself: + +```typescript +import { AuthService } from '@auth0/auth0-angular'; +import { HttpClient } from '@angular/common/http'; +import { switchMap } from 'rxjs/operators'; + +constructor(private auth: AuthService, private http: HttpClient) {} + +callApi() { + this.auth.getAccessTokenSilently({ + authorizationParams: { + audience: 'https://your-api-identifier' + } + }).pipe( + switchMap(token => + this.http.get('https://your-api.com/data', { + headers: { Authorization: `Bearer ${token}` } + }) + ) + ).subscribe({ + next: (response) => console.log(response), + error: (err) => console.error(err) + }); +} +``` + +--- + +## User Profile Component + +```typescript +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-profile', + standalone: true, + imports: [CommonModule], + template: ` +
+ +

{{ user.name }}

+

{{ user.email }}

+
{{ user | json }}
+
+ ` +}) +export class ProfileComponent { + constructor(public auth: AuthService) {} +} +``` + +--- + +## Error Handling + +### Handle Auth Errors + +```typescript +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; + +@Component({ + template: ` +
+

Authentication Error

+

{{ error.message }}

+
+ ` +}) +export class AppComponent implements OnInit { + error$ = this.auth.error$; + + constructor(private auth: AuthService) {} + + ngOnInit() { + this.error$.subscribe(error => { + if (error) { + console.error('Auth error:', error); + } + }); + } +} +``` + +--- + +## Common Patterns + +### Login with Options + +```typescript +login() { + this.auth.loginWithRedirect({ + authorizationParams: { + connection: 'google-oauth2', + screen_hint: 'signup' + } + }); +} +``` + +## Testing + +### Mock AuthService + +```typescript +// auth.service.mock.ts +import { of } from 'rxjs'; + +export const mockAuthService = { + isAuthenticated$: of(true), + user$: of({ name: 'Test User', email: 'test@example.com' }), + loginWithRedirect: jasmine.createSpy('loginWithRedirect'), + logout: jasmine.createSpy('logout'), + getAccessTokenSilently: jasmine.createSpy('getAccessTokenSilently').and.returnValue(of('mock-token')) +}; +``` + +### Use in Tests + +```typescript +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AuthService } from '@auth0/auth0-angular'; +import { mockAuthService } from './auth.service.mock'; + +describe('AppComponent', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: AuthService, useValue: mockAuthService } + ] + }); + fixture = TestBed.createComponent(AppComponent); + }); + + it('should display user name', () => { + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('Test User'); + }); +}); +``` + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| CORS errors | Add URLs to "Allowed Web Origins" in Auth0 Dashboard | +| Interceptor not adding tokens | Verify `allowedList` in httpInterceptor config | +| Guard not redirecting | Ensure AuthService is provided in root | +| Observables not updating | Use `async` pipe or subscribe properly | + +--- + +## Next Steps + +- [API Reference](api.md) - Complete SDK documentation +- [Setup Guide](setup.md) - Installation and configuration +- [Main Skill](../SKILL.md) - Quick start workflow diff --git a/main/.mintlify/skills/auth0-angular/references/setup.md b/main/.mintlify/skills/auth0-angular/references/setup.md new file mode 100644 index 0000000000..cb3bcc525d --- /dev/null +++ b/main/.mintlify/skills/auth0-angular/references/setup.md @@ -0,0 +1,144 @@ +# Auth0 Angular Setup Guide + +Complete setup instructions for Angular applications. + +--- + +## Quick Setup (Automated) + +### Bash Script + +```bash +#!/bin/bash + +# Install Auth0 CLI +if ! command -v auth0 &> /dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install auth0/auth0-cli/auth0 + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Download and review the install script before executing + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh + echo "⚠️ Review the install script at /tmp/auth0-install.sh before running" + sh /tmp/auth0-install.sh -b /usr/local/bin + rm /tmp/auth0-install.sh + fi +fi + +# Login to Auth0 +if ! auth0 tenants list &> /dev/null; then + echo "Auth0 Login Required" + read -p "Do you have an Auth0 account? (y/n): " HAS_ACCOUNT + if [[ "$HAS_ACCOUNT" != "y" ]]; then + echo "Visit https://auth0.com/signup to create an account" + read -p "Press Enter when ready..." + fi + auth0 login +fi + +# Create or select app +auth0 apps list +read -p "Enter your Auth0 app ID (or press Enter to create new): " APP_ID + +if [ -z "$APP_ID" ]; then + APP_NAME="${PWD##*/}-angular-app" + APP_ID=$(auth0 apps create \ + --name "$APP_NAME" \ + --type spa \ + --auth-method None \ + --callbacks "http://localhost:4200" \ + --logout-urls "http://localhost:4200" \ + --origins "http://localhost:4200" \ + --web-origins "http://localhost:4200" \ + --metadata "created_by=agent_skills" \ + --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) +fi + +# Get credentials +AUTH0_DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) +AUTH0_CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + +echo "✅ Configuration complete!" +echo "Update src/environments/environment.ts with:" +echo " domain: '$AUTH0_DOMAIN'" +echo " clientId: '$AUTH0_CLIENT_ID'" +``` + +--- + +## Manual Setup + +### Step 1: Install SDK + +```bash +npm install @auth0/auth0-angular +``` + +### Step 2: Configure Environment + +Create or update `src/environments/environment.ts`: + +```typescript +export const environment = { + production: false, + auth0: { + domain: 'your-tenant.auth0.com', + clientId: 'your-client-id', + authorizationParams: { + redirect_uri: window.location.origin + } + } +}; +``` + +For production (`src/environments/environment.prod.ts`): + +```typescript +export const environment = { + production: true, + auth0: { + domain: 'your-tenant.auth0.com', + clientId: 'your-client-id', + authorizationParams: { + redirect_uri: 'https://your-production-domain.com' + } + } +}; +``` + +### Step 3: Get Auth0 Credentials + +Using Auth0 CLI: + +```bash +auth0 login +auth0 apps list +auth0 apps show +``` + +Or via [Auth0 Dashboard](https://manage.auth0.com): +1. Create Single Page Application +2. Configure callback URLs: `http://localhost:4200` +3. Copy domain and client ID + +--- + +## Troubleshooting + +**Module not found errors:** +- Ensure @auth0/auth0-angular is in package.json +- Run `npm install` + +**CORS errors:** +- Add `http://localhost:4200` to "Allowed Web Origins" in Auth0 Dashboard + +**Environment variables not working:** +- Angular uses environment files, not .env +- Rebuild app after changing environment files + +--- + +## Next Steps + +- [Integration Guide](integration.md) - Implementation patterns +- [API Reference](api.md) - Complete SDK documentation +- [Main Skill](../SKILL.md) - Quick start guide diff --git a/main/.mintlify/skills/auth0-aspnetcore-api/SKILL.md b/main/.mintlify/skills/auth0-aspnetcore-api/SKILL.md index 3a84b49086..55ee8c2cc8 100644 --- a/main/.mintlify/skills/auth0-aspnetcore-api/SKILL.md +++ b/main/.mintlify/skills/auth0-aspnetcore-api/SKILL.md @@ -4,6 +4,10 @@ description: "Use when securing ASP.NET Core Web API endpoints with JWT Bearer t license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 ASP.NET Core Web API Integration @@ -190,6 +194,7 @@ Built-in proof-of-possession token binding per RFC 9449. See [Integration Guide] - `auth0-quickstart` - Basic Auth0 setup - `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-cli` - Manage Auth0 resources from the terminal --- diff --git a/main/.mintlify/skills/auth0-aspnetcore-api/references/api.md b/main/.mintlify/skills/auth0-aspnetcore-api/references/api.md new file mode 100644 index 0000000000..71dd3d8d88 --- /dev/null +++ b/main/.mintlify/skills/auth0-aspnetcore-api/references/api.md @@ -0,0 +1,187 @@ +# Auth0 ASP.NET Core Web API - API Reference + +Complete reference for Auth0.AspNetCore.Authentication.Api configuration options and extension methods. + +--- + +## Extension Methods + +### `AddAuth0ApiAuthentication` + +Registers Auth0 JWT Bearer authentication with the dependency injection container. + +```csharp +builder.Services.AddAuth0ApiAuthentication(options => +{ + options.Domain = "your-tenant.auth0.com"; + options.JwtBearerOptions = new JwtBearerOptions + { + Audience = "https://my-api.example.com" + }; +}); +``` + +--- + +## Auth0ApiAuthenticationOptions + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `Domain` | `string` | Yes | Auth0 tenant domain. Format: `your-tenant.auth0.com` (no `https://` prefix) | +| `JwtBearerOptions` | `JwtBearerOptions` | Yes | Microsoft JWT Bearer options. Set `Audience` here. | + +### `Domain` + +Your Auth0 tenant domain. The library constructs the authority URL as `https://{Domain}/`. + +```csharp +options.Domain = builder.Configuration["Auth0:Domain"]; +// e.g., "dev-abc123.us.auth0.com" +``` + +### `JwtBearerOptions` + +Full access to the underlying [Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions). + +Key sub-properties: + +| Property | Type | Description | +|----------|------|-------------| +| `Audience` | `string` | API Identifier from Auth0. Must exactly match. | +| `TokenValidationParameters` | `TokenValidationParameters` | Additional token validation rules | +| `Events` | `JwtBearerEvents` | Hooks into authentication lifecycle | +| `SaveToken` | `bool` | Whether to save the raw token in the auth properties | +| `RequireHttpsMetadata` | `bool` | Defaults to `true` in production | +| `IncludeErrorDetails` | `bool` | Include error details in 401/403 responses | + +--- + +## Auth0ApiAuthenticationBuilder + +Returned by `AddAuth0ApiAuthentication`. Fluent builder for additional configuration. + +### `.WithDPoP()` + +Enables DPoP token validation with default settings (Allowed mode). + +```csharp +builder.Services.AddAuth0ApiAuthentication(options => { ... }) + .WithDPoP(); +``` + +### `.WithDPoP(Action configureDPoP)` + +Enables DPoP with custom configuration. + +```csharp +builder.Services.AddAuth0ApiAuthentication(options => { ... }) + .WithDPoP(dpop => + { + dpop.Mode = DPoPModes.Required; + dpop.IatOffset = 300; + dpop.Leeway = 30; + }); +``` + +--- + +## DPoPOptions + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `Mode` | `DPoPModes` | `Allowed` | Controls which token types are accepted | +| `IatOffset` | `int` | `0` | Allowed clock skew in seconds for the `iat` claim | +| `Leeway` | `int` | `0` | Additional leeway in seconds for token time validation | + +### DPoPModes Enum + +| Value | Description | +|-------|-------------| +| `DPoPModes.Allowed` | Accept both DPoP-bound and standard Bearer tokens | +| `DPoPModes.Required` | Only accept DPoP-bound tokens; reject standard Bearer | +| `DPoPModes.Disabled` | Disable DPoP; standard JWT Bearer only | + +--- + +## ASP.NET Core Authorization + +Auth0 does not provide custom authorization attributes. Use standard ASP.NET Core authorization: + +### Policy-Based Authorization + +```csharp +// Register policies +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("read:messages", policy => + policy.RequireClaim("scope", "read:messages")); +}); + +// Apply to Minimal API +app.MapGet("/endpoint", handler).RequireAuthorization("read:messages"); + +// Apply to controller action +[Authorize(Policy = "read:messages")] +public IActionResult GetMessages() { ... } +``` + +### Attribute-Based Authorization + +```csharp +// Require any authenticated user +[Authorize] +public IActionResult Private() { ... } + +// Require specific policy +[Authorize(Policy = "read:messages")] +public IActionResult Messages() { ... } + +// Allow anonymous on an otherwise protected controller +[AllowAnonymous] +public IActionResult Public() { ... } +``` + +--- + +## JwtBearerEvents Hooks + +Configure callbacks for authentication lifecycle events: + +| Event | When | Common Use | +|-------|------|------------| +| `OnTokenValidated` | After token is validated | Extract custom claims, enrich identity | +| `OnAuthenticationFailed` | Token validation fails | Custom logging, error responses | +| `OnChallenge` | 401 response about to be sent | Customize 401 response body | +| `OnForbidden` | 403 response about to be sent | Customize 403 response body | +| `OnMessageReceived` | Before token extraction | Extract token from non-standard location | + +**Example - Custom 401 response:** + +```csharp +options.JwtBearerOptions = new JwtBearerOptions +{ + Audience = "...", + Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.HandleResponse(); + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + return context.Response.WriteAsJsonAsync(new + { + error = "unauthorized", + error_description = "A valid access token is required." + }); + } + } +}; +``` + +--- + +## References + +- [Auth0 ASP.NET Core Web API Quickstart](https://auth0.com/docs/quickstart/backend/aspnet-core-webapi) +- [SDK GitHub Repository](https://github.com/auth0/aspnetcore-api) +- [Microsoft JWT Bearer Documentation](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwtbearer) diff --git a/main/.mintlify/skills/auth0-aspnetcore-api/references/integration.md b/main/.mintlify/skills/auth0-aspnetcore-api/references/integration.md new file mode 100644 index 0000000000..0c882b9cee --- /dev/null +++ b/main/.mintlify/skills/auth0-aspnetcore-api/references/integration.md @@ -0,0 +1,360 @@ +# Auth0 ASP.NET Core Web API Integration Patterns + +Advanced integration patterns for ASP.NET Core Web API applications. + +--- + +## Scope-Based Authorization + +### Define Authorization Policies + +In `Program.cs`, add policies that map to Auth0 API permissions: + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("read:messages", policy => + policy.RequireClaim("scope", "read:messages")); + + options.AddPolicy("write:messages", policy => + policy.RequireClaim("scope", "write:messages")); + + options.AddPolicy("manage:orders", policy => + { + policy.RequireClaim("scope", "read:orders"); + policy.RequireClaim("scope", "write:orders"); + }); +}); +``` + +### Apply Policies to Endpoints + +**Minimal API:** + +```csharp +app.MapGet("/api/messages", (HttpContext ctx) => +{ + return Results.Ok(new { messages = new[] { "Hello", "World" } }); +}).RequireAuthorization("read:messages"); + +app.MapPost("/api/messages", (HttpContext ctx) => +{ + return Results.Created("/api/messages/1", new { id = 1 }); +}).RequireAuthorization("write:messages"); +``` + +**Controller-based:** + +```csharp +[ApiController] +[Route("api/messages")] +public class MessagesController : ControllerBase +{ + [HttpGet] + [Authorize(Policy = "read:messages")] + public IActionResult GetMessages() => + Ok(new { messages = new[] { "Hello", "World" } }); + + [HttpPost] + [Authorize(Policy = "write:messages")] + public IActionResult CreateMessage() => + Created("/api/messages/1", new { id = 1 }); +} +``` + +### Define Permissions in Auth0 + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Click the **Permissions** tab +4. Add permissions matching your policy names (e.g., `read:messages`, `write:messages`) + +### Request Tokens with Scopes + +Clients must request tokens that include the required scopes: + +```bash +# Client Credentials with specific scopes +curl -X POST https://your-tenant.auth0.com/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "audience": "https://my-api.example.com", + "grant_type": "client_credentials", + "scope": "read:messages write:messages" + }' +``` + +--- + +## DPoP Support + +DPoP (Demonstrating Proof of Possession, RFC 9449) binds tokens to a specific client key pair, preventing token theft. + +### Enable DPoP + +```csharp +builder.Services.AddAuth0ApiAuthentication(options => +{ + options.Domain = builder.Configuration["Auth0:Domain"]; + options.JwtBearerOptions = new JwtBearerOptions + { + Audience = builder.Configuration["Auth0:Audience"] + }; +}) +.WithDPoP(); // Accept both DPoP and Bearer tokens (Allowed mode) +``` + +### DPoP Required Mode + +To reject standard Bearer tokens and accept only DPoP-bound tokens: + +```csharp +.WithDPoP(dpopOptions => +{ + dpopOptions.Mode = DPoPModes.Required; +}); +``` + +Optionally configure clock skew tolerance: + +```csharp +.WithDPoP(dpopOptions => +{ + dpopOptions.Mode = DPoPModes.Required; + dpopOptions.IatOffset = 300; // Allow 5-minute clock skew for iat claim + dpopOptions.Leeway = 30; // 30-second leeway for token validation +}); +``` + +### DPoP Modes + +| Mode | Behavior | +|------|----------| +| `DPoPModes.Allowed` (default) | Accept both DPoP-bound and standard Bearer tokens | +| `DPoPModes.Required` | Only accept DPoP-bound tokens; reject standard Bearer | +| `DPoPModes.Disabled` | Standard JWT Bearer only, DPoP disabled | + +### Enable DPoP on Auth0 API + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Enable **Allow Skipping User Consent** and enable DPoP binding requirement + +--- + +## Accessing User Claims + +### From HttpContext in Minimal API + +```csharp +app.MapGet("/api/profile", (HttpContext ctx) => +{ + var userId = ctx.User.FindFirst("sub")?.Value; + var email = ctx.User.FindFirst("https://example.com/email")?.Value; // custom claim + var scopes = ctx.User.FindFirst("scope")?.Value?.Split(' ') ?? []; + + return Results.Ok(new { userId, scopes }); +}).RequireAuthorization(); +``` + +### From Controller + +```csharp +[Authorize] +[HttpGet("profile")] +public IActionResult GetProfile() +{ + var userId = User.FindFirst("sub")?.Value; + var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? []; + + return Ok(new { userId, scopes }); +} +``` + +### Common JWT Claims + +| Claim | Description | +|-------|-------------| +| `sub` | User ID (subject) | +| `scope` | Space-separated list of granted scopes | +| `aud` | Audience (your API identifier) | +| `iss` | Issuer (your Auth0 tenant URL) | +| `exp` | Expiration timestamp | +| `iat` | Issued-at timestamp | + +Custom claims added via Auth0 Actions use namespaced keys, e.g., `https://your-domain.com/role`. + +--- + +## Error Handling + +### Return Problem Details for Auth Errors + +```csharp +builder.Services.AddProblemDetails(); + +// Customize auth error responses +builder.Services.AddAuth0ApiAuthentication(options => +{ + options.Domain = builder.Configuration["Auth0:Domain"]; + options.JwtBearerOptions = new JwtBearerOptions + { + Audience = builder.Configuration["Auth0:Audience"], + Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.HandleResponse(); + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + return context.Response.WriteAsJsonAsync(new + { + error = "unauthorized", + error_description = "A valid access token is required." + }); + }, + OnForbidden = context => + { + context.Response.StatusCode = 403; + context.Response.ContentType = "application/json"; + return context.Response.WriteAsJsonAsync(new + { + error = "insufficient_scope", + error_description = "The access token does not have the required scopes." + }); + } + } + }; +}); +``` + +### Standard Error Responses + +| Status | Cause | Fix | +|--------|-------|-----| +| 401 | Missing or invalid token | Include valid `Authorization: Bearer ` header | +| 401 | Expired token | Request a fresh access token | +| 401 | Wrong audience | Token's `aud` claim must match your API Identifier | +| 403 | Insufficient scope | Token must include required scopes | + +--- + +## Mixed Public and Protected Endpoints + +```csharp +// Public - no auth needed +app.MapGet("/api/public", () => + Results.Ok(new { message = "Public endpoint" })); + +// Protected - requires valid JWT +app.MapGet("/api/private", (HttpContext ctx) => + Results.Ok(new { message = "Private endpoint", userId = ctx.User.FindFirst("sub")?.Value })) + .RequireAuthorization(); + +// Protected with scope +app.MapGet("/api/messages", (HttpContext ctx) => + Results.Ok(new { messages = Array.Empty() })) + .RequireAuthorization("read:messages"); +``` + +--- + +## Custom Token Validation + +For advanced scenarios, configure additional JWT validation parameters: + +```csharp +builder.Services.AddAuth0ApiAuthentication(options => +{ + options.Domain = builder.Configuration["Auth0:Domain"]; + options.JwtBearerOptions = new JwtBearerOptions + { + Audience = builder.Configuration["Auth0:Audience"], + TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "sub", // Map sub claim to User.Identity.Name + ClockSkew = TimeSpan.FromSeconds(30) + } + }; +}); +``` + +--- + +## Testing + +### Integration Testing with WebApplicationFactory + +```csharp +public class ApiTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + + public ApiTests(WebApplicationFactory factory) => + _factory = factory; + + [Fact] + public async Task PublicEndpoint_Returns200() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/public"); + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task ProtectedEndpoint_WithoutToken_Returns401() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/api/private"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task ProtectedEndpoint_WithValidToken_Returns200() + { + // Option 1: Real token from Auth0 CLI (requires network, good for integration tests) + // auth0 test token --audience https://my-api.example.com + // + // Option 2: Mock JWT for fast unit tests — override auth in WebApplicationFactory: + // _factory.WithWebHostBuilder(b => b.ConfigureTestServices(services => + // { + // services.PostConfigure(JwtBearerDefaults.AuthenticationScheme, o => + // { + // o.TokenValidationParameters = new TokenValidationParameters + // { + // ValidateIssuer = false, + // ValidateAudience = false, + // ValidateLifetime = false, + // SignatureValidator = (token, _) => new JwtSecurityToken(token) + // }; + // }); + // })); + // Then generate a token with: new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(...)) + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", "YOUR_TEST_TOKEN"); + + var response = await client.GetAsync("/api/private"); + response.EnsureSuccessStatusCode(); + } +} +``` + +--- + +## Security Considerations + +- **Never hardcode Domain or Audience** - Always use configuration (appsettings, User Secrets, environment variables) +- **Use HTTPS in production** - Auth0 requires HTTPS for token validation +- **Use minimal scopes** - Only request and enforce scopes your API actually needs +- **Keep packages updated** - Regularly update `Auth0.AspNetCore.Authentication.Api` for security patches + +--- + +## References + +- [API Reference](api.md) +- [Setup Guide](setup.md) +- [Main Skill](../SKILL.md) diff --git a/main/.mintlify/skills/auth0-aspnetcore-api/references/setup.md b/main/.mintlify/skills/auth0-aspnetcore-api/references/setup.md new file mode 100644 index 0000000000..fc2abe3b3f --- /dev/null +++ b/main/.mintlify/skills/auth0-aspnetcore-api/references/setup.md @@ -0,0 +1,140 @@ +# Auth0 ASP.NET Core Web API Setup Guide + +Setup instructions for ASP.NET Core Web API applications. + +--- + +## Quick Setup (Automated) + +Below uses the Auth0 CLI to create an Auth0 API resource and retrieve your credentials. + +### Step 1: Install Auth0 CLI and create API resource + +```bash +# Install Auth0 CLI (macOS) +brew install auth0/auth0-cli/auth0 + +# Login +auth0 login --no-input + +# Create an Auth0 API resource +auth0 apis create \ + --name "My ASP.NET Core API" \ + --identifier https://my-api.example.com \ + --json +``` + +Note the `identifier` value - this is your Audience. + +### Step 2: Add configuration + +Once you have your Domain and Audience, add the following to `appsettings.json`: + +```json +{ + "Auth0": { + "Domain": "your-tenant.auth0.com", + "Audience": "https://my-api.example.com" + } +} +``` + +Replace `your-tenant.auth0.com` with your Auth0 tenant domain and `https://my-api.example.com` with the identifier you used when creating the API resource. + +--- + +## Manual Setup + +### Install Package + +```bash +dotnet add package Auth0.AspNetCore.Authentication.Api +``` + +### Create Auth0 API Resource + +1. Go to Auth0 Dashboard → Applications → APIs +2. Click **Create API** +3. Set a **Name** and an **Identifier** (e.g., `https://my-api.example.com`) +4. Note the Identifier - this is your `Audience` + +### Configure appsettings.json + +```json +{ + "Auth0": { + "Domain": "your-tenant.auth0.com", + "Audience": "https://my-api.example.com" + } +} +``` + +**Important:** Domain format is `your-tenant.auth0.com` - do NOT include `https://`. + +### Get Auth0 Configuration + +- **Domain:** Auth0 Dashboard → Settings → Domain (or `auth0 tenants list`) +- **Audience:** The identifier you set when creating the API resource + +### Using Environment Variables + +For production/containers, set environment variables (these override appsettings.json): + +```bash +export Auth0__Domain=your-tenant.auth0.com +export Auth0__Audience=https://my-api.example.com +``` + +Note the double underscore `__` separator for nested config in environment variables. + +--- + +## Getting a Test Token + +### Via Auth0 Dashboard + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Click the **Test** tab +4. Click **Copy Token** to get a test access token + +### Via Auth0 CLI (Client Credentials) + +```bash +# Get access token for testing +auth0 test token \ + --audience https://my-api.example.com +``` + +### Via curl (Client Credentials Flow) + +```bash +curl -X POST https://your-tenant.auth0.com/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "audience": "https://my-api.example.com", + "grant_type": "client_credentials" + }' +``` + +--- + +## Troubleshooting + +**401 Unauthorized - "invalid_token":** Verify that the `Audience` in config exactly matches your API Identifier in Auth0 Dashboard. + +**401 Unauthorized - "invalid_issuer":** Ensure `Domain` does not include `https://` - use `your-tenant.auth0.com` format only. + +**HTTPS certificate errors locally:** Run `dotnet dev-certs https --trust` to trust the development certificate. + +**Token expired:** Test tokens from the Dashboard are short-lived. Request a fresh token. + +--- + +## Next Steps + +- [Integration Guide](integration.md) +- [API Reference](api.md) +- [Main Skill](../SKILL.md) diff --git a/main/.mintlify/skills/auth0-cli/SKILL.md b/main/.mintlify/skills/auth0-cli/SKILL.md index 07307de1a8..4766449cd4 100644 --- a/main/.mintlify/skills/auth0-cli/SKILL.md +++ b/main/.mintlify/skills/auth0-cli/SKILL.md @@ -33,7 +33,7 @@ The Auth0 CLI (`auth0`) lets you manage your tenant from the terminal. Install w ```bash auth0 login # interactive device-code login auth0 login --scopes "read:client_grants" # request extra scopes if 403 -auth0 login --domain .auth0.com --client-id --client-secret # CI/CD +auth0 login --domain .auth0.com --client-id --client-secret "$AUTH0_CLIENT_SECRET" # CI/CD ``` See [Authentication Details](references/cli.md#authentication) for machine login with JWT, tenant management, and logout. @@ -72,12 +72,13 @@ Create or inspect Auth0 applications (client ID, secret, callback URLs, app type ```bash auth0 apps create --name "My SPA" --type spa \ + --auth-method None \ --callbacks "http://localhost:3000" \ --logout-urls "http://localhost:3000" \ --origins "http://localhost:3000" --json auth0 apps list --json-compact -auth0 apps show --reveal-secrets --json +auth0 apps show --json auth0 apps update --callbacks "http://localhost:3000,https://myapp.com" --json auth0 apps delete --force ``` @@ -110,7 +111,7 @@ Create, search, inspect, import, and manage users in your tenant. auth0 users search --query "email:user@example.com" --json auth0 users search-by-email user@example.com --json-compact auth0 users create --connection-name "Username-Password-Authentication" \ - --email "test@example.com" --password "SecureP@ss!" --json + --email "test@example.com" --password "$USER_PASSWORD" --json auth0 users show --json auth0 users blocks list --json auth0 users blocks unblock diff --git a/main/.mintlify/skills/auth0-cli/references/cli.md b/main/.mintlify/skills/auth0-cli/references/cli.md index b5d30d4b1c..60ba87045a 100644 --- a/main/.mintlify/skills/auth0-cli/references/cli.md +++ b/main/.mintlify/skills/auth0-cli/references/cli.md @@ -40,7 +40,7 @@ auth0 login auth0 login --scopes "read:client_grants,create:client_grants" # Machine login with client secret (CI/CD, non-interactive) -auth0 login --domain .auth0.com --client-id --client-secret +auth0 login --domain .auth0.com --client-id --client-secret "$AUTH0_CLIENT_SECRET" # Machine login with private key JWT auth0 login --domain .auth0.com --client-id \ @@ -120,6 +120,7 @@ auth0 apps show --reveal-secrets --json-compact ```bash # SPA (React, Vue, Angular — browser-only, no backend) auth0 apps create --name "My SPA" --type spa \ + --auth-method None \ --callbacks "http://localhost:3000" \ --logout-urls "http://localhost:3000" \ --origins "http://localhost:3000" \ @@ -136,7 +137,7 @@ auth0 apps create --name "My Web App" --type regular \ auth0 apps create --name "My API Service" --type m2m --json # Native (iOS, Android, React Native) -auth0 apps create --name "My Mobile App" --type native --json +auth0 apps create --name "My Mobile App" --type native --auth-method None --json # Resource Server (API + client in one entity) auth0 apps create --name "My API Client" --type resource_server \ @@ -327,7 +328,7 @@ Simpler alternative to `search` when you just need an exact email match. auth0 users create \ --connection-name "Username-Password-Authentication" \ --email "testuser@example.com" \ - --password "SecureP@ss123!" \ + --password "$USER_PASSWORD" \ --name "Test User" \ --json ``` diff --git a/main/.mintlify/skills/auth0-expo/SKILL.md b/main/.mintlify/skills/auth0-expo/SKILL.md index e89e4d49b4..f456ec2e18 100644 --- a/main/.mintlify/skills/auth0-expo/SKILL.md +++ b/main/.mintlify/skills/auth0-expo/SKILL.md @@ -4,6 +4,14 @@ description: Use when adding authentication to Expo (React Native) mobile apps license: Proprietary metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills + requires: + bins: + - gh + - node --- # Auth0 Expo Integration @@ -76,18 +84,11 @@ Add authentication to Expo (React Native) applications using `react-native-auth0 > > After the script completes, proceed to **Step 2 (Verify Expo Dev Client)**. -#### Manual Setup (User-Provided Credentials) +#### Manual Setup -> **Agent instruction:** Ask the user for their Auth0 credentials using `AskUserQuestion`: +> **Agent instruction:** If the user has not provided their Auth0 Domain and Client ID, ask them using `AskUserQuestion`. They can find both in the [Auth0 Dashboard](https://manage.auth0.com/) under **Applications > Applications > your app > Settings**. If they don't have an Auth0 app yet, they should create one with type **Native**. > -> "I need your Auth0 credentials to set up authentication. Please provide: -> -> 1. **Auth0 Domain** (e.g., `your-tenant.us.auth0.com`) -> 2. **Client ID** (a 32-character alphanumeric string) -> -> You can find both in the [Auth0 Dashboard](https://manage.auth0.com/) under **Applications > Applications > your app > Settings**. If you don't have an Auth0 app yet, create one with type **Native** and copy the domain and client ID from the settings page." -> -> Then write the configuration to app.json and proceed to **Step 2**. +> Once provided, configure the Auth0 plugin in app.json and proceed to **Step 2**. ### 2. Verify Expo Dev Client @@ -260,6 +261,7 @@ export default function App() { - [auth0-quickstart](/auth0-quickstart) — Set up an Auth0 account and application - [auth0-react-native](/auth0-react-native) — Bare React Native CLI projects - [auth0-mfa](/auth0-mfa) — Configure multi-factor authentication +- [auth0-cli](/auth0-cli) — Manage Auth0 resources from the terminal ## References diff --git a/main/.mintlify/skills/auth0-expo/references/api.md b/main/.mintlify/skills/auth0-expo/references/api.md new file mode 100644 index 0000000000..a0fc4d7190 --- /dev/null +++ b/main/.mintlify/skills/auth0-expo/references/api.md @@ -0,0 +1,197 @@ +# auth0-expo API Reference & Testing + +## Table of Contents + +- [Configuration Reference](#configuration-reference) — Auth0Provider props, authorize/clearSession/getCredentials options +- [Expo Config Plugin Reference](#expo-config-plugin-reference) — app.json plugin fields and auto-configuration +- [User Profile Claims](#user-profile-claims) — Standard OIDC claims +- [Credentials Object](#credentials-object) — Token properties +- [Testing Checklist](#testing-checklist) — Dev build, platform-specific, Auth0 config, EAS +- [Common Issues](#common-issues) — Error table with causes and solutions +- [Security Considerations](#security-considerations) — PKCE, secure storage, custom scheme, tokens, network + +## Configuration Reference + +### Auth0Provider Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `domain` | `string` | Yes | Auth0 tenant domain (e.g., `your-tenant.auth0.com`) | +| `clientId` | `string` | Yes | Auth0 application Client ID | +| `localAuthenticationOptions` | `LocalAuthenticationOptions` | No | Biometric authentication configuration | +| `maxRetries` | `number` | No | Credential renewal retry count (iOS only, default: 0) | +| `useDPoP` | `boolean` | No | Enable DPoP token binding (default: true) | +| `headers` | `Record` | No | Custom headers for all API requests | + +### authorize() Options + +**First argument (parameters):** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `scope` | `string` | OAuth scopes (default: `openid profile email`) | +| `audience` | `string` | API identifier for access token | +| `organization` | `string` | Organization ID for enterprise login | +| `invitationUrl` | `string` | Organization invitation URL | +| `connection` | `string` | Force a specific connection (e.g., `google-oauth2`) | +| `additionalParameters` | `object` | Extra parameters for the /authorize endpoint | + +**Second argument (options):** + +| Option | Type | Description | +|--------|------|-------------| +| `customScheme` | `string` | **Required for Expo.** URL scheme matching app.json plugin config. | + +### clearSession() Options + +| Option | Type | Description | +|--------|------|-------------| +| `customScheme` | `string` | **Required for Expo.** Must match the scheme used in authorize(). | +| `federated` | `boolean` | If true, also logs out from the identity provider | + +### getCredentials() Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `scope` | `string` | Minimum required scope | +| `minTtl` | `number` | Minimum time-to-live in seconds for the access token | +| `parameters` | `object` | Additional parameters | +| `forceRefresh` | `boolean` | Force token refresh even if not expired | + +## Expo Config Plugin Reference + +### app.json Plugin Configuration + +```json +["react-native-auth0", { + "domain": "your-tenant.auth0.com", + "customScheme": "auth0sample" +}] +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `domain` | `string` | Yes | Auth0 tenant domain | +| `customScheme` | `string` | No | Custom URL scheme (lowercase, no special chars). If `"https"`, enables Android App Links with `autoVerify`. | + +**What the plugin configures automatically:** +- **iOS**: Adds URL scheme to Info.plist (`CFBundleURLSchemes`), adds deep linking handler to AppDelegate +- **Android**: Sets `manifestPlaceholders` (`auth0Domain`, `auth0Scheme`) in build.gradle + +## User Profile Claims + +| Claim | Type | Description | +|-------|------|-------------| +| `sub` | `string` | Unique user identifier | +| `name` | `string` | Full name | +| `nickname` | `string` | Display name | +| `email` | `string` | Email address | +| `email_verified` | `boolean` | Whether email is verified | +| `picture` | `string` | Profile picture URL | +| `updated_at` | `string` | Last profile update timestamp | +| `org_id` | `string` | Organization ID (if using Organizations) | + +## Credentials Object + +| Property | Type | Description | +|----------|------|-------------| +| `accessToken` | `string` | Access token for API calls | +| `idToken` | `string` | ID token with user claims | +| `refreshToken` | `string` | Refresh token (if `offline_access` requested) | +| `tokenType` | `string` | Token type (`Bearer` or `DPoP`) | +| `expiresAt` | `number` | Token expiration timestamp | +| `scope` | `string` | Granted scopes | + +## Testing Checklist + +### Development Build Testing + +- [ ] Login flow: Tap login → browser opens → complete login → app shows user info +- [ ] Logout flow: Tap logout → session cleared → app shows login button +- [ ] Credential persistence: Close app → reopen → user remains logged in +- [ ] Token refresh: Wait for token expiry → `getCredentials()` returns fresh token +- [ ] Error handling: Cancel login → app handles USER_CANCELLED gracefully +- [ ] Loading state: `isLoading` is true until auth state is determined + +### Platform-Specific Testing + +- [ ] **iOS Simulator**: Login/logout works, URL scheme redirects correctly +- [ ] **Android Emulator**: Login/logout works, custom scheme callback received +- [ ] **Physical iOS Device**: Test on a real physical device — Face ID / Touch ID prompts work (if biometrics enabled). Note: biometric authentication is not available on simulators. +- [ ] **Physical Android Device**: Test on a real physical device — fingerprint / PIN prompts work (if biometrics enabled). Test deep link redirection from browser back to app. + +### Auth0 Configuration Testing + +- [ ] Callback URL matches exactly (lowercase, no trailing slash) +- [ ] Application type is **Native** in Auth0 Dashboard +- [ ] Allowed Callback URLs include both iOS and Android URLs +- [ ] Allowed Logout URLs include both iOS and Android URLs +- [ ] OIDC Conformant toggle is enabled in Advanced OAuth settings + +### EAS Build Testing + +- [ ] Development build: `eas build --profile development` succeeds +- [ ] Config plugin applied: Native files contain Auth0 configuration after prebuild +- [ ] Production build: `eas build --profile production` succeeds + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| "Invariant Violation: Native module cannot be null" | Using Expo Go instead of development build | Run `npx expo run:ios` or `npx expo run:android`, or create a development build with EAS | +| App hangs after login | Callback URL mismatch | Verify callback URL is lowercase, no trailing slash, and matches Auth0 Dashboard exactly | +| Login opens but redirects fail | Missing customScheme in authorize call | Pass `{ customScheme: 'your-scheme' }` as second argument to `authorize()` | +| "PKCE not allowed" error | App type is not Native | Change application type to **Native** in Auth0 Dashboard | +| Blank screen after authentication | React Navigation interference | Ensure Auth0Provider wraps the entire navigation container | +| Android build fails with manifest errors | Conflicting auth0Domain placeholders | Remove manual manifest changes — let the Expo config plugin handle it | +| iOS build fails with pod errors | Stale native projects | Run `npx expo prebuild --clean` to regenerate native code | +| Token refresh fails silently | Missing `offline_access` scope | Include `offline_access` in the scope parameter during login | +| Biometric prompt not showing | Simulator limitation | Test biometrics on a physical device — simulators have limited biometric support | + +## Security Considerations + +### PKCE (Proof Key for Code Exchange) + +The SDK uses PKCE by default for all Web Auth flows. PKCE protects against authorization code interception attacks. No additional configuration is needed. + +### Secure Credential Storage + +Credentials are stored securely: +- **iOS**: Encrypted in the Keychain +- **Android**: Encrypted in SharedPreferences via SecureCredentialsManager + +Never store tokens manually in AsyncStorage, MMKV, or other unencrypted storage. + +### Custom Scheme Security + +Custom URL schemes can be subject to [client impersonation attacks](https://datatracker.ietf.org/doc/html/rfc8252#section-8.6). For production apps, consider using: +- **Android App Links** (`customScheme: "https"`) — requires SHA256 fingerprint configuration +- **iOS Universal Links** — requires Associated Domains and Apple Developer account + +### Token Handling Best Practices + +- Never log tokens to the console in production builds +- Use `getCredentials()` to access tokens — it auto-refreshes expired tokens +- Request `offline_access` scope for refresh token support +- Do not store tokens in React state — use `getCredentials()` on demand +- Enable DPoP for enhanced token security (enabled by default) + +### Network Security + +- All Auth0 API communication uses HTTPS +- The SDK validates ID token signatures, issuer, audience, and nonce +- Enable certificate pinning for additional security in high-security environments + +## Related Skills + +- [auth0-quickstart](/auth0-quickstart) — Set up an Auth0 account and application +- [auth0-react-native](/auth0-react-native) — Bare React Native CLI projects +- [auth0-mfa](/auth0-mfa) — Configure multi-factor authentication + +## References + +- [react-native-auth0 API Docs](https://auth0.github.io/react-native-auth0/) +- [Auth0 Expo Quickstart](https://auth0.com/docs/quickstart/native/react-native-expo/interactive) +- [Expo Config Plugins Guide](https://docs.expo.dev/guides/config-plugins/) +- [Auth0 Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login) +- [PKCE RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) diff --git a/main/.mintlify/skills/auth0-expo/references/integration.md b/main/.mintlify/skills/auth0-expo/references/integration.md new file mode 100644 index 0000000000..5e76315077 --- /dev/null +++ b/main/.mintlify/skills/auth0-expo/references/integration.md @@ -0,0 +1,500 @@ +# auth0-expo Integration Patterns + +## Table of Contents + +- [Web Auth Login](#web-auth-login) — Basic login with hooks, Auth0 class, audience, organizations +- [Web Auth Logout](#web-auth-logout) — Hook and class-based logout +- [Credential Management](#credential-management) — Retrieve, check, auto-refresh, Auth0 class +- [Biometric Authentication](#biometric-authentication) — Auth0Provider config, policies, Auth0 class +- [DPoP](#dpop-demonstrating-proof-of-possession) — Enable, API calls, token migration +- [Multi-Resource Refresh Tokens](#multi-resource-refresh-tokens-mrrt) — Multiple API access +- [Custom Token Exchange](#custom-token-exchange-rfc-8693) — External provider tokens +- [Native to Web SSO](#native-to-web-sso) — Session transfer to web apps +- [Organization Invitations](#organization-invitations) — Deep link handling +- [Error Handling](#error-handling) — WebAuth errors, Credentials Manager errors +- [Credential Renewal Retry](#credential-renewal-retry-ios) — iOS retry with backoff +- [Using Custom Headers](#using-custom-headers) — Custom API request headers + +## Web Auth Login + +The primary authentication method uses Auth0 Universal Login via the system browser. The `useAuth0` hook provides the `authorize` method. + +### Basic Login with Hooks + +```typescript +import { useAuth0 } from 'react-native-auth0'; + +function LoginScreen() { + const { authorize, user, isLoading, error } = useAuth0(); + + const login = async () => { + try { + await authorize( + { scope: 'openid profile email' }, + { customScheme: 'auth0sample' } + ); + } catch (e) { + console.error('Login error:', e); + } + }; + + if (isLoading) return ; + + return ( + + {!user && + ); +} +``` + +### Custom Hook + +```typescript +import { useAuth0 } from '@auth0/auth0-react'; +import { useCallback, useState } from 'react'; + +interface StepUpOptions { + maxAge?: number; +} + +export function useStepUpAuth() { + const { getAccessTokenSilently, getIdTokenClaims, loginWithRedirect } = useAuth0(); + const [isVerifying, setIsVerifying] = useState(false); + + const hasMFA = useCallback(async (): Promise => { + const claims = await getIdTokenClaims(); + const amr = claims?.amr || []; + return amr.includes('mfa'); + }, [getIdTokenClaims]); + + const requireMFA = useCallback(async (options: StepUpOptions = {}) => { + setIsVerifying(true); + try { + const mfaCompleted = await hasMFA(); + + if (!mfaCompleted) { + // Try silent step-up first + try { + await getAccessTokenSilently({ + authorizationParams: { + acr_values: 'http://schemas.openid.net/pape/policies/2007/06/multi-factor', + max_age: options.maxAge ?? 0, + }, + cacheMode: 'off', + }); + } catch { + // Silent failed, redirect to MFA + await loginWithRedirect({ + authorizationParams: { + acr_values: 'http://schemas.openid.net/pape/policies/2007/06/multi-factor', + max_age: options.maxAge ?? 0, + }, + }); + return false; + } + } + + return true; + } finally { + setIsVerifying(false); + } + }, [getAccessTokenSilently, loginWithRedirect, hasMFA]); + + return { requireMFA, hasMFA, isVerifying }; +} + +// Usage +function TransferFunds() { + const { requireMFA, isVerifying } = useStepUpAuth(); + + const handleTransfer = async () => { + const verified = await requireMFA(); + if (verified) { + // Proceed with transfer + } + }; + + return ( + + ); +} +``` + +--- + +## Next.js (App Router) + +### API Route + +```typescript +// app/api/sensitive/route.ts +import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'; +import { NextResponse } from 'next/server'; + +export const POST = withApiAuthRequired(async function handler(req) { + const session = await getSession(); + + // Check if MFA was completed + const amr = session?.user?.amr || []; + + if (!amr.includes('mfa')) { + return NextResponse.json( + { error: 'MFA required', code: 'mfa_required' }, + { status: 403 } + ); + } + + // Proceed with sensitive operation + return NextResponse.json({ success: true }); +}); +``` + +### Client Component + +```typescript +// app/transfer/page.tsx +'use client'; + +import { useUser } from '@auth0/nextjs-auth0/client'; +import { useRouter } from 'next/navigation'; + +export default function TransferPage() { + const { user } = useUser(); + const router = useRouter(); + + const handleTransfer = async () => { + const response = await fetch('/api/sensitive', { method: 'POST' }); + + if (response.status === 403) { + const { code } = await response.json(); + if (code === 'mfa_required') { + // Redirect to login with MFA required + router.push('/api/auth/login?acr_values=http://schemas.openid.net/pape/policies/2007/06/multi-factor'); + return; + } + } + + // Success + }; + + return ; +} +``` + +--- + +## Vue.js + +```typescript + + + +``` + +--- + +## Angular + +```typescript +import { Component, inject } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; +import { firstValueFrom } from 'rxjs'; + +@Component({ + selector: 'app-sensitive-action', + template: ` + + ` +}) +export class SensitiveActionComponent { + private auth = inject(AuthService); + isVerifying = false; + + private async hasMFA(): Promise { + const claims = await firstValueFrom(this.auth.idTokenClaims$); + const amr = (claims as any)?.amr || []; + return amr.includes('mfa'); + } + + async handleSensitiveAction() { + this.isVerifying = true; + try { + if (!(await this.hasMFA())) { + // Request MFA + this.auth.loginWithRedirect({ + authorizationParams: { + acr_values: 'http://schemas.openid.net/pape/policies/2007/06/multi-factor', + max_age: 0, + }, + }); + return; + } + + // MFA verified, proceed + console.log('MFA verified, proceeding...'); + } finally { + this.isVerifying = false; + } + } +} +``` diff --git a/main/.mintlify/skills/auth0-migration/SKILL.md b/main/.mintlify/skills/auth0-migration/SKILL.md index a39d4fd27f..7ed81ff227 100644 --- a/main/.mintlify/skills/auth0-migration/SKILL.md +++ b/main/.mintlify/skills/auth0-migration/SKILL.md @@ -4,6 +4,22 @@ description: Use when migrating or switching from an existing auth provider (Fir license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + 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 Migration Guide diff --git a/main/.mintlify/skills/auth0-migration/references/code-patterns.md b/main/.mintlify/skills/auth0-migration/references/code-patterns.md new file mode 100644 index 0000000000..b8db901804 --- /dev/null +++ b/main/.mintlify/skills/auth0-migration/references/code-patterns.md @@ -0,0 +1,731 @@ +# Code Migration Patterns + +Before/after code examples for migrating from common auth providers to Auth0 across different frameworks. + +--- + +## React Migration + +### Email/Password Authentication + +**Before (typical pattern):** +```typescript +// Old provider pattern +await signIn(email, password); +await signOut(); +const user = getCurrentUser(); +``` + +**After (Auth0):** +```typescript +import { useAuth0 } from '@auth0/auth0-react'; + +const { loginWithRedirect, logout, user, isAuthenticated } = useAuth0(); + +// Login triggers redirect to Auth0 Universal Login +loginWithRedirect(); + +// Logout with redirect +logout({ logoutParams: { returnTo: window.location.origin } }); + +// User available when authenticated +if (isAuthenticated) { + console.log(user.email); +} +``` + +--- + +### Auth State Listener + +**Before (typical pattern):** +```typescript +// Old provider pattern +onAuthStateChange((user) => { + if (user) { /* authenticated */ } + else { /* not authenticated */ } +}); +``` + +**After (Auth0):** +```typescript +import { useAuth0 } from '@auth0/auth0-react'; + +function App() { + const { isAuthenticated, isLoading, user } = useAuth0(); + + if (isLoading) return ; + + return isAuthenticated ? ( + + ) : ( + + ); +} +``` + +--- + +### Protected Routes + +**Before (typical pattern):** +```typescript +// Old provider pattern +function ProtectedRoute({ children }) { + const user = useCurrentUser(); + return user ? children : ; +} +``` + +**After (Auth0):** +```typescript +import { useAuth0 } from '@auth0/auth0-react'; + +function ProtectedRoute({ children }) { + const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0(); + + if (isLoading) return ; + + if (!isAuthenticated) { + loginWithRedirect(); + return null; + } + + return children; +} +``` + +--- + +### API Token Retrieval + +**Before (typical pattern):** +```typescript +// Old provider pattern +const token = await user.getIdToken(); +fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } }); +``` + +**After (Auth0):** +```typescript +import { useAuth0 } from '@auth0/auth0-react'; + +function ApiComponent() { + const { getAccessTokenSilently } = useAuth0(); + + const callApi = async () => { + const token = await getAccessTokenSilently(); + const response = await fetch('/api/data', { + headers: { Authorization: `Bearer ${token}` } + }); + return response.json(); + }; +} +``` + +--- + +## Next.js Migration + +### Middleware Protection + +**Before (typical pattern):** +```typescript +// Old provider middleware pattern +export function middleware(request) { + const session = getSession(request); + if (!session) return redirect('/login'); +} +``` + +**After (Auth0):** +```typescript +// middleware.ts +import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge'; + +export default withMiddlewareAuthRequired(); + +export const config = { + matcher: ['/dashboard/:path*', '/api/protected/:path*'] +}; +``` + +--- + +### Server Components (App Router) + +**Before (typical pattern):** +```typescript +// Old provider pattern +async function DashboardPage() { + const session = await getServerSession(); + if (!session) redirect('/login'); + + return
Welcome {session.user.name}
; +} +``` + +**After (Auth0):** +```typescript +import { getSession } from '@auth0/nextjs-auth0'; + +async function DashboardPage() { + const session = await getSession(); + if (!session) redirect('/api/auth/login'); + + return
Welcome {session.user.name}
; +} +``` + +--- + +### API Routes + +**Before (typical pattern):** +```typescript +// Old provider pattern +export async function GET(request) { + const session = await getSession(request); + if (!session) return new Response('Unauthorized', { status: 401 }); + + return Response.json({ data: 'protected' }); +} +``` + +**After (Auth0):** +```typescript +import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0'; + +export const GET = withApiAuthRequired(async function handler(req) { + const session = await getSession(); + return Response.json({ data: 'protected' }); +}); +``` + +--- + +## Express.js Migration + +### Server-Side Session Auth + +**Before (typical pattern):** +```typescript +// Old provider pattern with manual session +app.post('/login', async (req, res) => { + const user = await validateCredentials(req.body); + req.session.user = user; + res.redirect('/dashboard'); +}); + +app.get('/dashboard', (req, res) => { + if (!req.session.user) return res.redirect('/login'); + // ... +}); +``` + +**After (Auth0):** +```typescript +const { auth, requiresAuth } = require('express-openid-connect'); + +app.use(auth({ + authRequired: false, + auth0Logout: true, + secret: process.env.AUTH0_SECRET, + baseURL: process.env.AUTH0_BASE_URL, + clientID: process.env.AUTH0_CLIENT_ID, + issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL +})); + +// Auth0 handles /login, /logout, /callback automatically +app.get('/dashboard', requiresAuth(), (req, res) => { + // req.oidc.user contains the authenticated user + res.render('dashboard', { user: req.oidc.user }); +}); +``` + +--- + +### Express API Route Protection + +**Before (typical pattern):** +```typescript +// Old provider pattern +app.get('/api/data', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1]; + const user = await verifyToken(token); + + if (!user) return res.status(401).json({ error: 'Unauthorized' }); + + res.json({ data: 'protected' }); +}); +``` + +**After (Auth0):** +```typescript +const { auth } = require('express-oauth2-jwt-bearer'); + +const checkJwt = auth({ + audience: process.env.AUTH0_AUDIENCE, + issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL +}); + +app.get('/api/data', checkJwt, (req, res) => { + // req.auth contains verified token claims + res.json({ data: 'protected' }); +}); +``` + +--- + +## Vue.js Migration + +### Authentication + +**Before (typical pattern):** +```vue + +``` + +**After (Auth0):** +```vue + + + +``` + +--- + +### Vue Router Guards + +**Before (typical pattern):** +```typescript +// Old provider pattern +router.beforeEach(async (to, from, next) => { + const user = await getCurrentUser(); + + if (to.meta.requiresAuth && !user) { + next('/login'); + } else { + next(); + } +}); +``` + +**After (Auth0):** +```typescript +import { createAuthGuard } from '@auth0/auth0-vue'; + +router.beforeEach(createAuthGuard((to) => { + if (to.meta.requiresAuth) { + return true; // Requires authentication + } + return false; // Public route +})); +``` + +--- + +## Angular Migration + +### Authentication Service + +**Before (typical pattern):** +```typescript +// Old provider pattern +@Injectable({ providedIn: 'root' }) +export class AuthService { + async login() { + return await signIn(); + } + + async logout() { + return await signOut(); + } + + getCurrentUser() { + return this.currentUser$; + } +} +``` + +**After (Auth0):** +```typescript +import { AuthService } from '@auth0/auth0-angular'; +import { inject } from '@angular/core'; + +@Component({ + selector: 'app-auth', + template: ` +
+

Welcome {{ (auth.user$ | async)?.name }}

+ +
+ + + + ` +}) +export class AuthComponent { + auth = inject(AuthService); + + login() { + this.auth.loginWithRedirect(); + } + + logout() { + this.auth.logout({ logoutParams: { returnTo: window.location.origin } }); + } +} +``` + +--- + +### Route Guards + +**Before (typical pattern):** +```typescript +// Old provider pattern +@Injectable({ providedIn: 'root' }) +export class AuthGuard implements CanActivate { + canActivate(): boolean { + const user = this.authService.currentUser; + if (!user) { + this.router.navigate(['/login']); + return false; + } + return true; + } +} +``` + +**After (Auth0):** +```typescript +import { inject } from '@angular/core'; +import { AuthGuard } from '@auth0/auth0-angular'; + +const routes: Routes = [ + { + path: 'dashboard', + component: DashboardComponent, + canActivate: [AuthGuard] + } +]; +``` + +--- + +### HTTP Interceptor + +**Before (typical pattern):** +```typescript +// Old provider pattern +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler) { + const token = this.authService.getToken(); + + if (token) { + req = req.clone({ + setHeaders: { Authorization: `Bearer ${token}` } + }); + } + + return next.handle(req); + } +} +``` + +**After (Auth0):** +```typescript +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authHttpInterceptorFn } from '@auth0/auth0-angular'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient( + withInterceptors([authHttpInterceptorFn]) + ) + ] +}; +``` + +--- + +## React Native Migration + +### Authentication + +**Before (typical pattern):** +```typescript +// Old provider pattern +const [user, setUser] = useState(null); + +const login = async () => { + const result = await signIn(); + setUser(result.user); +}; + +const logout = async () => { + await signOut(); + setUser(null); +}; +``` + +**After (Auth0):** +```typescript +import Auth0 from 'react-native-auth0'; + +const auth0 = new Auth0({ + domain: process.env.AUTH0_DOMAIN, + clientId: process.env.AUTH0_CLIENT_ID +}); + +const [user, setUser] = useState(null); + +const login = async () => { + try { + const credentials = await auth0.webAuth.authorize({ + scope: 'openid profile email' + }); + setUser(credentials.idTokenPayload); + } catch (error) { + console.error('Login error:', error); + } +}; + +const logout = async () => { + try { + await auth0.webAuth.clearSession(); + setUser(null); + } catch (error) { + console.error('Logout error:', error); + } +}; +``` + +--- + +## Backend API JWT Validation + +### Node.js / Express + +**Before (typical pattern):** +```typescript +// Old provider pattern +import jwt from 'jsonwebtoken'; + +const verifyToken = (token) => { + return jwt.verify(token, process.env.JWT_SECRET, { + algorithms: ['HS256'] + }); +}; + +app.get('/api/protected', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1]; + const user = verifyToken(token); + res.json({ data: 'protected' }); +}); +``` + +**After (Auth0):** +```typescript +import jwt from 'jsonwebtoken'; +import { JwksClient } from 'jwks-rsa'; + +const client = new JwksClient({ + jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json` +}); + +async function validateToken(token) { + const decoded = jwt.decode(token, { complete: true }); + const key = await client.getSigningKey(decoded.header.kid); + + return jwt.verify(token, key.getPublicKey(), { + algorithms: ['RS256'], + audience: process.env.AUTH0_AUDIENCE, + issuer: `https://${process.env.AUTH0_DOMAIN}/` + }); +} + +app.get('/api/protected', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1]; + const user = await validateToken(token); + res.json({ data: 'protected' }); +}); +``` + +**Key Differences:** +- **Algorithm:** HS256 (symmetric) → RS256 (asymmetric) +- **Secret:** Shared secret → Public key from JWKS endpoint +- **Issuer:** Custom → Auth0 tenant URL +- **Audience:** Optional → Required for API validation + +--- + +### Python / Flask + +**Before (typical pattern):** +```python +# Old provider pattern +import jwt + +def verify_token(token): + return jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + +@app.route('/api/protected') +def protected(): + token = request.headers.get('Authorization').split(' ')[1] + user = verify_token(token) + return {'data': 'protected'} +``` + +**After (Auth0):** +```python +from jose import jwt +import requests + +def get_jwks(): + jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json" + return requests.get(jwks_url).json() + +def verify_token(token): + jwks = get_jwks() + unverified_header = jwt.get_unverified_header(token) + + # Find the key + rsa_key = {} + for key in jwks['keys']: + if key['kid'] == unverified_header['kid']: + rsa_key = { + 'kty': key['kty'], + 'kid': key['kid'], + 'use': key['use'], + 'n': key['n'], + 'e': key['e'] + } + + return jwt.decode( + token, + rsa_key, + algorithms=['RS256'], + audience=AUTH0_AUDIENCE, + issuer=f"https://{AUTH0_DOMAIN}/" + ) + +@app.route('/api/protected') +def protected(): + token = request.headers.get('Authorization').split(' ')[1] + user = verify_token(token) + return {'data': 'protected'} +``` + +--- + +## Provider-Specific Patterns + +### Firebase to Auth0 + +**Common Firebase patterns:** +```typescript +// Firebase +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; + +const auth = getAuth(); +const userCredential = await signInWithEmailAndPassword(auth, email, password); +const user = userCredential.user; +``` + +**Auth0 equivalent:** +```typescript +// Auth0 - uses redirect flow, not direct credentials +import { useAuth0 } from '@auth0/auth0-react'; + +const { loginWithRedirect } = useAuth0(); +await loginWithRedirect(); +``` + +**Note:** Auth0 uses Universal Login (redirect), not direct email/password submission for better security. + +--- + +### Supabase to Auth0 + +**Common Supabase patterns:** +```typescript +// Supabase +import { createClient } from '@supabase/supabase-js'; + +const supabase = createClient(url, key); +const { data, error } = await supabase.auth.signInWithPassword({ email, password }); +``` + +**Auth0 equivalent:** +```typescript +// Auth0 +import { useAuth0 } from '@auth0/auth0-react'; + +const { loginWithRedirect } = useAuth0(); +await loginWithRedirect(); +``` + +--- + +### Clerk to Auth0 + +**Common Clerk patterns:** +```typescript +// Clerk +import { useUser, useSignIn } from '@clerk/nextjs'; + +const { isSignedIn, user } = useUser(); +const { signIn } = useSignIn(); +``` + +**Auth0 equivalent:** +```typescript +// Auth0 +import { useUser } from '@auth0/nextjs-auth0/client'; + +const { user, error, isLoading } = useUser(); +const login = () => window.location.href = '/api/auth/login'; +``` + +--- + +## References + +- [Auth0 React SDK](https://auth0.com/docs/libraries/auth0-react) +- [Auth0 Next.js SDK](https://auth0.com/docs/libraries/nextjs) +- [Auth0 Vue SDK](https://auth0.com/docs/libraries/auth0-vue) +- [Auth0 Angular SDK](https://auth0.com/docs/libraries/auth0-angular) +- [Auth0 React Native SDK](https://auth0.com/docs/libraries/react-native-auth0) +- [Express OpenID Connect](https://auth0.com/docs/libraries/express-openid-connect) diff --git a/main/.mintlify/skills/auth0-migration/references/user-import.md b/main/.mintlify/skills/auth0-migration/references/user-import.md new file mode 100644 index 0000000000..cd1df2e5e9 --- /dev/null +++ b/main/.mintlify/skills/auth0-migration/references/user-import.md @@ -0,0 +1,587 @@ +# User Export and Import Guide + +Detailed guide for exporting users from existing auth providers and importing them to Auth0. + +--- + +## Exporting Users from Common Providers + +### Firebase + +**Via Firebase Console:** +1. Go to Authentication → Users +2. Click "..." menu → Export users +3. Downloads JSON file + +**Via Firebase CLI:** +```bash +firebase auth:export users.json --format=JSON +``` + +**Firebase user format:** +```json +{ + "users": [ + { + "localId": "user123", + "email": "user@example.com", + "emailVerified": true, + "passwordHash": "base64-encoded-hash", + "salt": "base64-encoded-salt", + "createdAt": "1234567890000" + } + ] +} +``` + +--- + +### AWS Cognito + +**Via AWS CLI:** +```bash +aws cognito-idp list-users \ + --user-pool-id us-east-1_ABC123 \ + --output json > users.json +``` + +**Via Node.js Script:** +```javascript +const AWS = require('aws-sdk'); +const cognito = new AWS.CognitoIdentityServiceProvider(); + +async function exportUsers() { + let users = []; + let paginationToken; + + do { + const response = await cognito.listUsers({ + UserPoolId: 'us-east-1_ABC123', + PaginationToken: paginationToken + }).promise(); + + users = users.concat(response.Users); + paginationToken = response.PaginationToken; + } while (paginationToken); + + return users; +} +``` + +--- + +### Supabase + +**Via Supabase SQL:** +```sql +-- Connect to Supabase database +SELECT + id, + email, + email_confirmed_at IS NOT NULL as email_verified, + encrypted_password, + created_at, + raw_user_meta_data +FROM auth.users; +``` + +**Export to JSON:** +```bash +psql $DATABASE_URL -c "COPY (SELECT row_to_json(t) FROM ( + SELECT id, email, encrypted_password, created_at + FROM auth.users +) t) TO STDOUT" > users.json +``` + +--- + +### Custom Database + +**Example SQL query:** +```sql +SELECT + id, + email, + email_verified, + password_hash, + created_at, + last_login, + metadata +FROM users +WHERE active = true; +``` + +**Export script (Node.js):** +```javascript +const { Pool } = require('pg'); + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + +async function exportUsers() { + const result = await pool.query(` + SELECT + id, + email, + email_verified, + password_hash, + created_at + FROM users + `); + + return result.rows.map(row => ({ + email: row.email, + email_verified: row.email_verified, + user_id: row.id, + created_at: row.created_at.toISOString() + })); +} +``` + +--- + +## Required User Data + +### Minimum Required Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `email` | ✅ Yes | User's email address | +| `email_verified` | ✅ Yes | Whether email is verified (true/false) | +| `user_id` | No | Original user ID (preserved for reference) | +| `password` | No* | Only if using password hash | +| `custom_password_hash` | No* | Password hash with algorithm | + +*Either `password` (plain text, not recommended) or `custom_password_hash` required for password-based users. + +### Optional Fields + +| Field | Description | +|-------|-------------| +| `given_name` | First name | +| `family_name` | Last name | +| `name` | Full name | +| `nickname` | Display name | +| `picture` | Profile picture URL | +| `created_at` | Account creation timestamp | +| `user_metadata` | Custom user data (editable by user) | +| `app_metadata` | Custom app data (not editable by user) | + +--- + +## Auth0 User Import Format + +### JSON Structure + +```json +[ + { + "email": "user@example.com", + "email_verified": true, + "user_id": "original-id-from-old-system", + "custom_password_hash": { + "algorithm": "bcrypt", + "hash": "$2a$10$abcdefghijklmnopqrstuv" + }, + "given_name": "John", + "family_name": "Doe", + "name": "John Doe", + "nickname": "johnd", + "picture": "https://example.com/avatar.jpg", + "user_metadata": { + "hobby": "reading", + "plan": "premium", + "migrated_from": "firebase" + }, + "app_metadata": { + "roles": ["admin"], + "permissions": ["read:users", "write:posts"] + } + } +] +``` + +--- + +## Password Hash Algorithms + +### Supported Algorithms + +Auth0 supports these password hashing algorithms: + +| Algorithm | Common Usage | Example | +|-----------|--------------|---------| +| `bcrypt` | Node.js, Ruby, PHP, Python | `$2a$10$...` | +| `argon2` | Modern apps, security-focused | `$argon2id$v=19$m=65536...` | +| `pbkdf2` | Python, Java | Requires iterations, key length | +| `sha256` | Legacy systems | Not recommended (weak) | +| `sha512` | Legacy systems | Not recommended (weak) | +| `md5` | Very old systems | Not recommended (very weak) | + +### bcrypt Format + +```json +{ + "custom_password_hash": { + "algorithm": "bcrypt", + "hash": "$2a$10$abcdefghijklmnopqrstuv" + } +} +``` + +**The hash includes:** +- `$2a$` - bcrypt identifier +- `10` - cost factor +- Rest - salt + hash + +--- + +### argon2 Format + +```json +{ + "custom_password_hash": { + "algorithm": "argon2", + "hash": { + "encoded": "$argon2id$v=19$m=65536,t=3,p=4$salt$hash" + } + } +} +``` + +--- + +### PBKDF2 Format + +```json +{ + "custom_password_hash": { + "algorithm": "pbkdf2", + "hash": { + "value": "base64-encoded-hash", + "encoding": "base64", + "key_length": 32, + "iterations": 10000, + "digest": "sha256" + } + } +} +``` + +--- + +### SHA-256/SHA-512 Format + +```json +{ + "custom_password_hash": { + "algorithm": "sha256", + "hash": { + "value": "hex-encoded-hash", + "encoding": "hex" + } + } +} +``` + +**Note:** Add salt if your system used salted hashes: +```json +{ + "custom_password_hash": { + "algorithm": "sha256", + "hash": { + "value": "hex-encoded-hash", + "encoding": "hex" + }, + "salt": { + "value": "hex-encoded-salt", + "encoding": "hex", + "position": "prefix" + } + } +} +``` + +--- + +## Importing to Auth0 + +### Method 1: Auth0 Dashboard + +**Steps:** +1. Go to Auth0 Dashboard +2. Navigate to **Authentication → Database → [Your Connection]** +3. Click **Users** tab +4. Click **Import Users** button +5. Upload your JSON file +6. Review and confirm + +**Limitations:** +- File size: Max 500KB per upload +- Users per file: Recommended max 10,000 + +--- + +### Method 2: Auth0 CLI + +**Prerequisites:** +```bash +# Install Auth0 CLI +brew install auth0/auth0-cli/auth0 + +# Login +auth0 login +``` + +**Import users:** +```bash +# Get connection ID +auth0 connections list + +# Import users +auth0 api post "jobs/users-imports" \ + --data "connection_id=con_ABC123" \ + --data "users=@users.json" +``` + +**Check import status:** +```bash +auth0 api get "jobs/{job-id}" +``` + +--- + +### Method 3: Management API + +**Using curl:** +```bash +curl -X POST "https://YOUR_DOMAIN.auth0.com/api/v2/jobs/users-imports" \ + -H "Authorization: Bearer YOUR_MGMT_API_TOKEN" \ + -H "Content-Type: multipart/form-data" \ + -F "users=@users.json" \ + -F "connection_id=con_ABC123" \ + -F "upsert=false" \ + -F "send_completion_email=true" +``` + +**Using Node.js:** +```javascript +const { ManagementClient } = require('auth0'); +const fs = require('fs'); + +const management = new ManagementClient({ + domain: process.env.AUTH0_DOMAIN, + clientId: process.env.AUTH0_CLIENT_ID, + clientSecret: process.env.AUTH0_CLIENT_SECRET +}); + +async function importUsers() { + const users = fs.readFileSync('users.json'); + + const job = await management.importUsers({ + connection_id: 'con_ABC123', + users: users, + upsert: false, + send_completion_email: true + }); + + console.log(`Import job created: ${job.id}`); + return job; +} +``` + +--- + +## Import Options + +### upsert + +- `true`: Update existing users, create new ones +- `false` (default): Only create new users, skip existing + +**When to use:** +- `upsert=true`: Re-running imports with updated data +- `upsert=false`: Initial migration, avoid accidental overwrites + +--- + +### send_completion_email + +- `true`: Email you when import completes +- `false`: No email notification + +**Useful for:** Large imports that take time + +--- + +### external_id + +Add to track which users were imported: + +```json +{ + "email": "user@example.com", + "external_id": "firebase:user123" +} +``` + +--- + +## Monitoring Import Progress + +### Check Job Status + +```bash +# Via CLI +auth0 api get "jobs/{job-id}" + +# Via Management API +curl "https://YOUR_DOMAIN.auth0.com/api/v2/jobs/{job-id}" \ + -H "Authorization: Bearer YOUR_MGMT_API_TOKEN" +``` + +**Response:** +```json +{ + "id": "job_abc123", + "type": "users_import", + "status": "processing", + "created_at": "2025-01-20T10:00:00.000Z", + "connection_id": "con_ABC123", + "summary": { + "total": 1000, + "inserted": 950, + "updated": 0, + "failed": 50 + } +} +``` + +**Status values:** +- `pending`: Job queued +- `processing`: Import in progress +- `completed`: Import finished successfully +- `failed`: Import failed + +--- + +### Download Error Report + +If import has failures: + +```bash +# Get errors file URL +auth0 api get "jobs/{job-id}/errors" + +# Download errors +curl "https://YOUR_DOMAIN.auth0.com/api/v2/jobs/{job-id}/errors" \ + -H "Authorization: Bearer YOUR_MGMT_API_TOKEN" \ + -o import-errors.json +``` + +**Error format:** +```json +[ + { + "user": { + "email": "invalid@example" + }, + "errors": [ + { + "code": "INVALID_EMAIL", + "message": "Email format is invalid" + } + ] + } +] +``` + +--- + +## Common Import Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| `INVALID_EMAIL` | Email format invalid | Validate and fix email format | +| `DUPLICATE_USER` | User already exists | Use `upsert=true` or skip | +| `INVALID_PASSWORD_HASH` | Hash format incorrect | Check algorithm and format | +| `MISSING_REQUIRED_FIELD` | Required field missing | Add email and email_verified | +| `CONNECTION_NOT_FOUND` | Invalid connection ID | Verify connection ID | +| `FILE_TOO_LARGE` | File exceeds limit | Split into smaller files | +| `INVALID_JSON` | JSON syntax error | Validate JSON format | + +--- + +## Best Practices + +### Prepare Your Data + +1. **Validate emails:** Remove invalid/duplicate emails +2. **Verify JSON:** Use JSON validator before upload +3. **Test with small batch:** Import 10-100 users first +4. **Backup original data:** Keep copy of export + +### Split Large Imports + +```bash +# Split into 5000-user chunks +split -l 5000 users.json users-chunk- + +# Import each chunk +for file in users-chunk-*; do + auth0 api post "jobs/users-imports" \ + --data "connection_id=con_ABC123" \ + --data "users=@$file" +done +``` + +### Add Migration Metadata + +Track migration for each user: + +```json +{ + "email": "user@example.com", + "user_metadata": { + "migrated": true, + "migrated_at": "2025-01-20T10:00:00.000Z", + "migrated_from": "firebase", + "original_id": "firebase-user-123" + } +} +``` + +--- + +## Post-Import Verification + +### Check User Count + +```bash +# Get total users in Auth0 +auth0 users list --number 1 + +# Or via API +curl "https://YOUR_DOMAIN.auth0.com/api/v2/users?per_page=1" \ + -H "Authorization: Bearer YOUR_MGMT_API_TOKEN" \ + | jq '.total' +``` + +### Test Login + +```bash +# Test user can login +auth0 test login --client-id YOUR_CLIENT_ID +``` + +### Verify Password Hashes Work + +Pick random users and attempt login to verify password hashes imported correctly. + +--- + +## References + +- [Auth0 Bulk User Import](https://auth0.com/docs/manage-users/user-migration/bulk-user-imports) +- [Password Hash Algorithms](https://auth0.com/docs/manage-users/user-migration/bulk-user-imports#password-hashing-algorithms) +- [Management API - User Import Job](https://auth0.com/docs/api/management/v2/jobs/post-users-imports) +- [User Import Best Practices](https://auth0.com/docs/manage-users/user-migration/bulk-user-imports#best-practices) diff --git a/main/.mintlify/skills/auth0-nextjs/SKILL.md b/main/.mintlify/skills/auth0-nextjs/SKILL.md index 8dadd36116..d963add2f5 100644 --- a/main/.mintlify/skills/auth0-nextjs/SKILL.md +++ b/main/.mintlify/skills/auth0-nextjs/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding authentication to Next.js applications (login, logo license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Next.js Integration @@ -244,6 +248,7 @@ Visit `http://localhost:3000` and test the login flow. - `auth0-quickstart` - Basic Auth0 setup - `auth0-migration` - Migrate from another auth provider - `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-cli` - Manage Auth0 resources from the terminal --- diff --git a/main/.mintlify/skills/auth0-nextjs/references/api.md b/main/.mintlify/skills/auth0-nextjs/references/api.md new file mode 100644 index 0000000000..b98086384d --- /dev/null +++ b/main/.mintlify/skills/auth0-nextjs/references/api.md @@ -0,0 +1,147 @@ +## Common Patterns + +### Custom Login with Options + +```typescript + + Login and go to Dashboard + +``` + +Or programmatically: + +```typescript +const router = useRouter(); +router.push('/auth/login?returnTo=/dashboard'); +``` + +--- + +### Get Access Token for External APIs + +```typescript +import { auth0 } from '@/lib/auth0'; +import { NextResponse } from 'next/server'; + +export async function GET() { + const { token } = await auth0.getAccessToken(); + + if (!token) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + const apiResponse = await fetch('https://external-api.com/data', { + headers: { + Authorization: `Bearer ${token}` + } + }); + + return NextResponse.json(await apiResponse.json()); +} +``` + +--- + +### Silent Authentication + +Users remain logged in across sessions automatically with refresh tokens. + +To force re-authentication: + +```typescript + + Force Re-login + +``` + +--- + +## Configuration Options + +### Advanced Auth0 Configuration + +Create `lib/auth0.ts`: + +```typescript +import { Auth0Client } from '@auth0/nextjs-auth0/server'; + +export const auth0 = new Auth0Client({ + domain: process.env.AUTH0_DOMAIN!, + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, + secret: process.env.AUTH0_SECRET!, + appBaseUrl: process.env.APP_BASE_URL!, + authorizationParameters: { + scope: 'openid profile email', + audience: process.env.AUTH0_AUDIENCE, + }, + routes: { + login: '/auth/login', + callback: '/auth/callback', + logout: '/auth/logout', + profile: '/auth/profile', + }, + session: { + rolling: true, + rollingDuration: 24 * 60 * 60, // 24 hours in seconds + absoluteDuration: 7 * 24 * 60 * 60, // 7 days in seconds + }, +}); +``` + +**Note:** Most configuration can be omitted - v4 uses sensible defaults. The middleware automatically mounts auth routes. + +--- + +## Testing + +1. Start your dev server: `npm run dev` +2. Visit `http://localhost:3000` +3. Click "Login" - redirects to Auth0 +4. Complete authentication +5. Verify redirect back with user session +6. Test protected pages and API routes +7. Click "Logout" and verify session cleared + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Missing required parameter: redirect_uri" | Ensure `APP_BASE_URL` is set correctly (v4 renamed from `AUTH0_BASE_URL`) | +| "Invalid state" error | Clear cookies/storage. Verify callback URL in Auth0 dashboard matches `APP_BASE_URL/auth/callback` | +| User session not persisting | Check `AUTH0_SECRET` is set and at least 32 characters | +| API routes return 401 | Check session with `auth0.getSession()` in route handler | +| Middleware loops infinitely | Ensure middleware matcher excludes `/auth/*` routes, not `/api/auth/*` | +| Import errors for v3 helpers | v4 removed `withApiAuthRequired` and `withPageAuthRequired` - use `auth0.getSession()` | +| Environment variable not recognized | v4 uses `AUTH0_DOMAIN` (no scheme) and `APP_BASE_URL`, not `AUTH0_ISSUER_BASE_URL` or `AUTH0_BASE_URL` | + +--- + +## Security Considerations + +- **Keep secrets secure** - Never commit `.env.local` to version control +- **Use HTTPS in production** - Auth0 requires secure callback URLs +- **Rotate secrets regularly** - Update `AUTH0_SECRET` periodically +- **Validate on server** - Always verify authentication server-side, not client-side +- **Configure CORS** - Set allowed origins in Auth0 application settings + +--- + +## Related Skills + +- `auth0-quickstart` - Initial Auth0 account setup +- `auth0-migration` - Migrate from another auth provider +- `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-organizations` - B2B multi-tenancy support +- `auth0-passkeys` - Add passkey authentication + +--- + +## References + +- [Auth0 Next.js SDK Documentation](https://auth0.com/docs/libraries/nextjs-auth0) +- [Auth0 Next.js SDK GitHub](https://github.com/auth0/nextjs-auth0) +- [Auth0 Next.js Quickstart](https://auth0.com/docs/quickstart/webapp/nextjs) +- [Next.js Middleware Documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware) diff --git a/main/.mintlify/skills/auth0-nextjs/references/integration.md b/main/.mintlify/skills/auth0-nextjs/references/integration.md new file mode 100644 index 0000000000..e07d3cd06e --- /dev/null +++ b/main/.mintlify/skills/auth0-nextjs/references/integration.md @@ -0,0 +1,261 @@ +# Auth0 Next.js Integration Patterns + +Server-side and client-side auth patterns for both App and Pages Router. + +--- + +## Protected Pages + +### App Router - Server Component + +```typescript +// app/profile/page.tsx +import { auth0 } from '@/lib/auth0'; +import { redirect } from 'next/navigation'; + +export default async function Profile() { + // In App Router, getSession() reads the request/response from Next.js' async context, + // so you don't pass req/res like in the Pages Router example below. + const session = await auth0.getSession(); + + if (!session) { + redirect('/auth/login?returnTo=/profile'); + } + + return ( +
+

Welcome, {session.user.name}!

+ {session.user.name} +
+ ); +} +``` + +### Pages Router - SSR + +```typescript +// pages/profile.tsx +import { auth0 } from '@/lib/auth0'; +import { GetServerSideProps } from 'next'; + +export default function Profile({ user }: { user: any }) { + return

Welcome, {user.name}!

; +} + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await auth0.getSession(req, res); + + if (!session) { + return { + redirect: { + destination: '/auth/login?returnTo=/profile', + permanent: false, + }, + }; + } + + return { + props: { user: session.user }, + }; +}; +``` + +--- + +## Protected API Routes + +### App Router + +```typescript +// app/api/private/route.ts +import { auth0 } from '@/lib/auth0'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const session = await auth0.getSession(); + + if (!session) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + return NextResponse.json({ data: 'Protected data', user: session.user }); +} +``` + +### Pages Router + +```typescript +// pages/api/private.ts +import { auth0 } from '@/lib/auth0'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await auth0.getSession(req, res); + + if (!session) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + res.json({ user: session.user }); +} +``` + +--- + +## Middleware (App Router) + +Protect multiple routes with middleware. + +**File placement:** If the project uses a `src/` directory (i.e. `src/app/` exists), place `middleware.ts` or `proxy.ts` inside `src/`. Otherwise, place at the project root. + +**Next.js 15** - Use `middleware.ts` (or `src/middleware.ts`): + +```typescript +// middleware.ts +import { NextRequest, NextResponse } from 'next/server'; +import { auth0 } from '@/lib/auth0'; + +export async function middleware(request: NextRequest) { + const authRes = await auth0.middleware(request); + + // Allow auth routes to be handled by SDK + if (request.nextUrl.pathname.startsWith('/auth')) { + return authRes; + } + + // Public routes + if (request.nextUrl.pathname === '/') { + return authRes; + } + + // Protected routes - check session + const session = await auth0.getSession(request); + + if (!session) { + const { origin } = new URL(request.url); + return NextResponse.redirect(`${origin}/auth/login?returnTo=${request.nextUrl.pathname}`); + } + + return authRes; +} + +export const config = { + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', + ], +}; +``` + +**Next.js 16** - Use either `middleware.ts` (same as above) or `proxy.ts` (same `src/` placement rules): + +```typescript +// proxy.ts +import { NextRequest, NextResponse } from 'next/server'; +import { auth0 } from '@/lib/auth0'; + +export async function proxy(request: NextRequest) { + const authRes = await auth0.middleware(request); + + // Allow auth routes to be handled by SDK + if (request.nextUrl.pathname.startsWith('/auth')) { + return authRes; + } + + // Public routes + if (request.nextUrl.pathname === '/') { + return authRes; + } + + // Protected routes - check session + const session = await auth0.getSession(request); + + if (!session) { + const { origin } = new URL(request.url); + return NextResponse.redirect(`${origin}/auth/login?returnTo=${request.nextUrl.pathname}`); + } + + return authRes; +} + +export const config = { + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', + ], +}; +``` + +--- + +## Calling External APIs + +### App Router - Server Action + +```typescript +// app/actions.ts +'use server'; + +import { auth0 } from '@/lib/auth0'; + +export async function getData() { + const { token } = await auth0.getAccessToken(); + + if (!token) { + throw new Error('No access token available'); + } + + const response = await fetch('https://your-api.com/data', { + headers: { Authorization: `Bearer ${token}` } + }); + + return response.json(); +} +``` + +### Pages Router - API Route + +```typescript +// pages/api/data.ts +import { auth0 } from '@/lib/auth0'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await auth0.getSession(req, res); + + if (!session) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const { token } = await auth0.getAccessToken(req, res); + + if (!token) { + return res.status(401).json({ error: 'No access token' }); + } + + const response = await fetch('https://your-api.com/data', { + headers: { Authorization: `Bearer ${token}` } + }); + + const data = await response.json(); + res.json(data); +} +``` + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Invalid state" error | Regenerate `AUTH0_SECRET`, clear cookies | +| Client secret required | Next.js uses Regular Web Application type | +| Callback URL mismatch | Add `/auth/callback` to Allowed Callback URLs (v4 dropped `/api` prefix) | +| Middleware not protecting routes | Ensure middleware calls `auth0.middleware()` and check `matcher` config | +| Routes return 404 | v4 uses `/auth/*` paths, not `/api/auth/*` - update all auth links | + +--- + +## Next Steps + +- [API Reference](api.md) - Complete SDK documentation +- [Setup Guide](setup.md) - Installation guide +- [Main Skill](../SKILL.md) - Quick start diff --git a/main/.mintlify/skills/auth0-nextjs/references/setup.md b/main/.mintlify/skills/auth0-nextjs/references/setup.md new file mode 100644 index 0000000000..e93cb9c3b3 --- /dev/null +++ b/main/.mintlify/skills/auth0-nextjs/references/setup.md @@ -0,0 +1,172 @@ +# Auth0 Next.js Setup Guide + +Setup instructions for Next.js with App Router or Pages Router. + +--- + +## Quick Setup (Automated) + +**Never read the contents of `.env.local` or `.env` at any point during setup.** The file may contain sensitive secrets that should not be exposed in the LLM context. If you determine you need to read the file for any reason, ask the user for explicit permission before doing so — do not proceed until the user confirms. + +**Before running any part of this setup that writes to an env file, you MUST ask the user for explicit confirmation.** Follow the steps below precisely. + +### Step 1: Check for existing env files and confirm with user + +Before writing credentials, check which env files exist: + +```bash +test -f .env.local && echo "ENV_LOCAL_EXISTS" || echo "ENV_LOCAL_NOT_FOUND" +test -f .env && echo "ENV_EXISTS" || echo "ENV_NOT_FOUND" +``` + +Then ask the user for explicit confirmation before proceeding — do not continue until the user confirms: + +- If `.env.local` exists, ask: + - Question: "A `.env.local` file already exists and may contain secrets unrelated to Auth0. This setup will append Auth0 credentials to it without modifying existing content. Do you want to proceed?" + - Options: "Yes, append to existing .env.local" / "No, I'll update it manually" + +- If `.env.local` does **not** exist but `.env` exists, ask: + - Question: "A `.env` file already exists and may contain secrets unrelated to Auth0. This setup will append Auth0 credentials to it without modifying existing content. Do you want to proceed?" + - Options: "Yes, append to existing .env" / "No, I'll update it manually" + +- If neither exists, ask: + - Question: "This setup will create a `.env.local` file containing Auth0 credentials (AUTH0_CLIENT_ID, AUTH0_DOMAIN, AUTH0_SECRET) and a placeholder for AUTH0_CLIENT_SECRET that you will need to fill in manually. Do you want to proceed?" + - Options: "Yes, create .env.local" / "No, I'll configure it manually" + +**Do not proceed with writing to any env file unless the user selects the confirmation option.** + +### Step 2: Run automated setup (only after confirmation) + +```bash +#!/bin/bash + +# Install Auth0 CLI +if ! command -v auth0 &> /dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install auth0/auth0-cli/auth0 + else + # Download and review the install script before executing + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh + echo "⚠️ Review the install script at /tmp/auth0-install.sh before running" + sh /tmp/auth0-install.sh -b /usr/local/bin + rm /tmp/auth0-install.sh + fi +fi + +# Login +if ! auth0 tenants list &> /dev/null; then + echo "Visit https://auth0.com/signup if you need an account" + auth0 login +fi + +# Create/select app +auth0 apps list +read -p "Enter app ID (or Enter to create new): " APP_ID + +if [ -z "$APP_ID" ]; then + APP_ID=$(auth0 apps create \ + --name "${PWD##*/}-nextjs" \ + --type regular \ + --callbacks "http://localhost:3000/auth/callback" \ + --logout-urls "http://localhost:3000" \ + --metadata "created_by=agent_skills" \ + --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) +fi + +# Get credentials +AUTH0_DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) +AUTH0_CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + +# Generate secret +AUTH0_SECRET=$(openssl rand -hex 32) + +# Determine target env file +if [ -f .env.local ]; then + TARGET_FILE=".env.local" +elif [ -f .env ]; then + TARGET_FILE=".env" +else + TARGET_FILE=".env.local" +fi + +# Append Auth0 credentials +cat >> "$TARGET_FILE" << ENVEOF +AUTH0_SECRET=$AUTH0_SECRET +APP_BASE_URL=http://localhost:3000 +AUTH0_DOMAIN=$AUTH0_DOMAIN +AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID +AUTH0_CLIENT_SECRET='YOUR_CLIENT_SECRET' +ENVEOF + +echo "✅ Auth0 credentials written to $TARGET_FILE" +``` + +After the script runs, remind the user to: +1. Open the env file that was written and replace `YOUR_CLIENT_SECRET` with the actual client secret from Auth0. +2. Ensure the env file is listed in `.gitignore` to avoid accidentally committing secrets. + +--- + +## Manual Setup + +### Step 1: Install SDK + +```bash +npm install @auth0/nextjs-auth0 +``` + +### Step 2: Create .env.local + +```bash +AUTH0_SECRET= +APP_BASE_URL=http://localhost:3000 +AUTH0_DOMAIN=your-tenant.auth0.com +AUTH0_CLIENT_ID=your-client-id +AUTH0_CLIENT_SECRET=your-client-secret +``` + +Generate `AUTH0_SECRET`: +```bash +openssl rand -hex 32 +``` + +### Step 3: Configure Auth0 Application + +Via CLI: +```bash +auth0 login +auth0 apps create --name "My Next.js App" --type regular \ + --callbacks "http://localhost:3000/auth/callback" \ + --logout-urls "http://localhost:3000" +``` + +Via Dashboard: +1. Create **Regular Web Application** +2. Configure: + - Allowed Callback URLs: `http://localhost:3000/auth/callback` + - Allowed Logout URLs: `http://localhost:3000` +3. Copy credentials to `.env.local` + +--- + +## Troubleshooting + +**"Invalid state" error:** +- Regenerate `AUTH0_SECRET` +- Clear cookies and restart dev server + +**Client secret not working:** +- Next.js uses Regular Web Application (not SPA) +- Verify client secret copied correctly + +**Callback URL mismatch:** +- Ensure `/auth/callback` is in Allowed Callback URLs +- Check `APP_BASE_URL` matches your domain + +--- + +## Next Steps + +- [Integration Guide](integration.md) - Implementation patterns +- [API Reference](api.md) - Complete SDK documentation +- [Main Skill](../SKILL.md) - Quick start diff --git a/main/.mintlify/skills/auth0-nuxt/SKILL.md b/main/.mintlify/skills/auth0-nuxt/SKILL.md index 0629d52879..2424bbb512 100644 --- a/main/.mintlify/skills/auth0-nuxt/SKILL.md +++ b/main/.mintlify/skills/auth0-nuxt/SKILL.md @@ -4,6 +4,10 @@ description: Use when implementing Auth0 authentication in Nuxt 3/4 applications license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Nuxt SDK @@ -224,4 +228,11 @@ export default defineEventHandler(async (event) => { **Guides:** [Route Protection Patterns](./references/route-protection.md) • [Custom Session Stores](./references/session-stores.md) • [Common Examples](./references/examples.md) +## Related Skills + +- `auth0-quickstart` - Basic Auth0 setup +- `auth0-cli` - Manage Auth0 resources from the terminal + +--- + **Links:** [Auth0-Nuxt GitHub](https://github.com/auth0/auth0-nuxt) • [Auth0 Docs](https://auth0.com/docs) • [Nuxt Modules](https://nuxt.com/modules) diff --git a/main/.mintlify/skills/auth0-nuxt/references/examples.md b/main/.mintlify/skills/auth0-nuxt/references/examples.md new file mode 100644 index 0000000000..e944bb428d --- /dev/null +++ b/main/.mintlify/skills/auth0-nuxt/references/examples.md @@ -0,0 +1,557 @@ +# Common Patterns and Examples + +Real-world patterns and complete examples for Auth0-Nuxt implementations. + +## Basic App Layout + +### Navigation with Conditional Login/Logout + +```vue + + + + +``` + +### App Layout with User Avatar + +```vue + + + + +``` + +## User Profile Page + +```vue + + + + +``` + +## Protected API Calls + +### Fetching User-Specific Data + +```typescript +// server/api/user/profile.get.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const user = await auth0Client.getUser(); + + if (!user) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized' + }); + } + + // Fetch user profile from database + const profile = await getUserProfile(user.sub); + + return { profile }; +}); +``` + +### Calling External API with Access Token + +```typescript +// server/api/external/data.get.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const { accessToken } = await auth0Client.getAccessToken(); + + const response = await $fetch('https://api.example.com/data', { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return response; +}); +``` + +### Client-Side API Call Pattern + +```vue + + + + +``` + +## Role-Based UI + +### Conditional Rendering by Role + +```vue + + + +``` + +### Composable for Role Checking + +```typescript +// composables/useRoles.ts +export const useRoles = () => { + const user = useUser(); + + const hasRole = (role: string) => { + if (!user.value) return false; + const roles = user.value['https://my-app.com/roles'] || []; + return roles.includes(role); + }; + + const hasAnyRole = (roles: string[]) => { + return roles.some(role => hasRole(role)); + }; + + const hasAllRoles = (roles: string[]) => { + return roles.every(role => hasRole(role)); + }; + + return { + hasRole, + hasAnyRole, + hasAllRoles + }; +}; +``` + +Usage: + +```vue + + + +``` + +## Multi-Tenant Applications + +### Tenant Selection After Login + +```typescript +// server/api/auth/callback.get.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const { appState } = await auth0Client.completeInteractiveLogin( + new URL(event.node.req.url, useRuntimeConfig().auth0.appBaseUrl) + ); + + const user = await auth0Client.getUser(); + const tenants = user?.['https://my-app.com/tenants'] || []; + + // If user has multiple tenants, redirect to tenant selection + if (tenants.length > 1) { + return sendRedirect(event, '/select-tenant'); + } + + // Single tenant, set and redirect + if (tenants.length === 1) { + // Store tenant in session or cookie + setCookie(event, 'tenant-id', tenants[0]); + } + + return sendRedirect(event, appState?.returnTo || '/'); +}); +``` + +### Tenant-Based Data Isolation + +```typescript +// server/api/data.get.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const user = await auth0Client.getUser(); + + if (!user) { + throw createError({ statusCode: 401 }); + } + + const tenantId = getCookie(event, 'tenant-id'); + + if (!tenantId) { + throw createError({ + statusCode: 400, + statusMessage: 'No tenant selected' + }); + } + + // Verify user has access to this tenant + const tenants = user['https://my-app.com/tenants'] || []; + if (!tenants.includes(tenantId)) { + throw createError({ + statusCode: 403, + statusMessage: 'Access denied to this tenant' + }); + } + + return getTenantData(tenantId); +}); +``` + +## Organization Support + +### Organization Login + +```vue + + + + +``` + +### Custom Login Handler with Organization + +```typescript +// server/routes/auth/org-login.get.ts +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const organization = query.organization as string; + + if (!organization) { + throw createError({ + statusCode: 400, + statusMessage: 'Organization parameter required' + }); + } + + const auth0Client = useAuth0(event); + const authUrl = await auth0Client.startInteractiveLogin({ + authorizationParams: { + organization: organization + }, + appState: { + returnTo: query.returnTo || '/' + } + }); + + return sendRedirect(event, authUrl.href); +}); +``` + +## Impersonation Support + +### Start Impersonation + +```typescript +// server/api/admin/impersonate.post.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const admin = await auth0Client.getUser(); + + if (!admin) { + throw createError({ statusCode: 401 }); + } + + // Check admin permission + const permissions = admin['https://my-app.com/permissions'] || []; + if (!permissions.includes('impersonate:users')) { + throw createError({ statusCode: 403 }); + } + + const body = await readBody(event); + const { userId } = body; + + // Store impersonation in session + const session = await auth0Client.getSession(); + if (session) { + session.impersonating = { + adminId: admin.sub, + userId: userId + }; + } + + return { success: true }; +}); +``` + +### End Impersonation + +```typescript +// server/api/admin/stop-impersonate.post.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (session?.impersonating) { + delete session.impersonating; + } + + return { success: true }; +}); +``` + +## Progressive Enhancement + +### Login Link with Fallback + +```vue + + + +``` + +## Error Handling + +### Global Error Handler + +```typescript +// server/middleware/error-handler.server.ts +export default defineEventHandler(async (event) => { + try { + // Continue to next middleware/handler + } catch (error) { + console.error('Authentication error:', error); + + if (error.statusCode === 401) { + return sendRedirect(event, '/auth/login'); + } + + throw error; + } +}); +``` + +### Client-Side Error Boundary + +```vue + + + + +``` + +## Loading States + +### Auth Loading Component + +```vue + + + + +``` + +## Logging and Monitoring + +### Audit Log Middleware + +```typescript +// server/middleware/audit-log.server.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const user = await auth0Client.getUser(); + + if (user) { + console.log({ + timestamp: new Date().toISOString(), + userId: user.sub, + path: event.node.req.url, + method: event.node.req.method, + ip: getRequestIP(event); + }); + } +}); +``` + +## Token Refresh Handling + +```typescript +// server/api/sensitive-data.get.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + + try { + const { accessToken } = await auth0Client.getAccessToken(); + + return await $fetch('https://api.example.com/sensitive', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + } catch (error) { + // Token might be expired, SDK handles refresh automatically + // If refresh fails, session is invalid + if (error.statusCode === 401) { + throw createError({ + statusCode: 401, + statusMessage: 'Session expired, please log in again' + }); + } + throw error; + } +}); +``` diff --git a/main/.mintlify/skills/auth0-nuxt/references/route-protection.md b/main/.mintlify/skills/auth0-nuxt/references/route-protection.md new file mode 100644 index 0000000000..3b6c26c89c --- /dev/null +++ b/main/.mintlify/skills/auth0-nuxt/references/route-protection.md @@ -0,0 +1,528 @@ +# Route Protection Patterns for Auth0-Nuxt + +Comprehensive guide for protecting routes, pages, and API endpoints in Nuxt 3/4 applications with Auth0. + +## Protection Layers + +Auth0-Nuxt provides three protection layers: + +1. **Route Middleware** - Client-side navigation guards (pages) +2. **Server Middleware** - SSR protection (server-rendered routes) +3. **API Route Guards** - Protect API endpoints + +## Route Middleware (Client-Side Navigation) + +### Basic Route Middleware + +Create `middleware/auth.ts`: + +```typescript +export default defineNuxtRouteMiddleware((to, from) => { + const user = useUser(); + + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } +}); +``` + +### Apply to Specific Pages + +```vue + + + + +``` + +### Role-Based Middleware + +```typescript +// middleware/admin.ts +export default defineNuxtRouteMiddleware((to, from) => { + const user = useUser(); + + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } + + // Check for admin role + const roles = user.value['https://my-app.com/roles'] || []; + if (!roles.includes('admin')) { + return navigateTo('/unauthorized'); + } +}); +``` + +```vue + + +``` + +### Permission-Based Middleware + +```typescript +// middleware/permissions.ts +export default defineNuxtRouteMiddleware((to, from) => { + const user = useUser(); + + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } + + // Check for specific permission + const permissions = user.value['https://my-app.com/permissions'] || []; + const requiredPermission = to.meta.permission; + + if (requiredPermission && !permissions.includes(requiredPermission)) { + return navigateTo('/forbidden'); + } +}); +``` + +```vue + + +``` + +### Multiple Middleware Chain + +```vue + +``` + +## Server Middleware (SSR Protection) + +### Global Server Middleware + +```typescript +// server/middleware/auth.server.ts +export default defineEventHandler(async (event) => { + const url = getRequestURL(event); + + // Skip auth routes + if (url.pathname.startsWith('/auth/')) { + return; + } + + // Protect all /dashboard routes + if (url.pathname.startsWith('/dashboard')) { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + return sendRedirect(event, `/auth/login?returnTo=${encodeURIComponent(url.pathname)}`); + } + } +}); +``` + +### Path-Specific Protection + +```typescript +// server/middleware/auth.server.ts +const protectedPaths = ['/dashboard', '/profile', '/settings']; + +export default defineEventHandler(async (event) => { + const url = getRequestURL(event); + + const isProtected = protectedPaths.some(path => + url.pathname.startsWith(path) + ); + + if (isProtected) { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + return sendRedirect(event, `/auth/login?returnTo=${encodeURIComponent(url.pathname)}`); + } + } +}); +``` + +### Role-Based Server Protection + +```typescript +// server/middleware/admin-routes.server.ts +export default defineEventHandler(async (event) => { + const url = getRequestURL(event); + + if (url.pathname.startsWith('/admin')) { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + return sendRedirect(event, `/auth/login?returnTo=${encodeURIComponent(url.pathname)}`); + } + + const user = await auth0Client.getUser(); + const roles = user?.['https://my-app.com/roles'] || []; + + if (!roles.includes('admin')) { + throw createError({ + statusCode: 403, + statusMessage: 'Forbidden: Admin access required' + }); + } + } +}); +``` + +## API Route Protection + +### Basic API Protection + +```typescript +// server/api/user-data.ts +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized' + }); + } + + return { data: 'protected user data' }; +}); +``` + +### Reusable Auth Guard + +```typescript +// server/utils/require-auth.ts +export async function requireAuth(event: H3Event) { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized' + }); + } + + return session; +} +``` + +Use in API routes: + +```typescript +// server/api/protected.ts +export default defineEventHandler(async (event) => { + await requireAuth(event); + return { message: 'This is protected' }; +}); +``` + +### Permission-Based API Protection + +```typescript +// server/utils/require-permission.ts +export async function requirePermission( + event: H3Event, + permission: string +) { + const auth0Client = useAuth0(event); + const user = await auth0Client.getUser(); + + if (!user) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized' + }); + } + + const permissions = user['https://my-app.com/permissions'] || []; + + if (!permissions.includes(permission)) { + throw createError({ + statusCode: 403, + statusMessage: `Forbidden: Missing permission '${permission}'` + }); + } + + return user; +} +``` + +Use in API routes: + +```typescript +// server/api/billing/invoices.get.ts +export default defineEventHandler(async (event) => { + await requirePermission(event, 'read:billing'); + + return { invoices: [] }; +}); +``` + +### Global API Middleware + +```typescript +// server/middleware/api-auth.server.ts +export default defineEventHandler(async (event) => { + const url = getRequestURL(event); + + // Protect all API routes except public ones + if (url.pathname.startsWith('/api/')) { + const publicRoutes = ['/api/health', '/api/version']; + + if (!publicRoutes.includes(url.pathname)) { + const auth0Client = useAuth0(event); + const session = await auth0Client.getSession(); + + if (!session) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized' + }); + } + } + } +}); +``` + +## Advanced Patterns + +### Conditional Protection by Environment + +```typescript +// middleware/auth.ts +export default defineNuxtRouteMiddleware((to, from) => { + const config = useRuntimeConfig(); + + // Skip auth in development + if (config.public.environment === 'development') { + return; + } + + const user = useUser(); + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } +}); +``` + +### Subscription-Based Protection + +```typescript +// middleware/subscription.ts +export default defineNuxtRouteMiddleware((to, from) => { + const user = useUser(); + + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } + + const subscription = user.value['https://my-app.com/subscription']; + + if (!subscription || subscription.status !== 'active') { + return navigateTo('/subscribe'); + } +}); +``` + +### Rate Limiting Protection + +```typescript +// server/middleware/rate-limit.server.ts +const rateLimitMap = new Map(); + +export default defineEventHandler(async (event) => { + const auth0Client = useAuth0(event); + const user = await auth0Client.getUser(); + + if (!user) { + return; + } + + const userId = user.sub; + const now = Date.now(); + const limit = rateLimitMap.get(userId); + + if (!limit || now > limit.resetAt) { + rateLimitMap.set(userId, { + count: 1, + resetAt: now + 60000 // 1 minute + }); + } else { + limit.count++; + + if (limit.count > 100) { + throw createError({ + statusCode: 429, + statusMessage: 'Too Many Requests' + }); + } + } +}); +``` + +### Email Verification Required + +```typescript +// middleware/verified.ts +export default defineNuxtRouteMiddleware((to, from) => { + const user = useUser(); + + if (!user.value) { + return navigateTo(`/auth/login?returnTo=${encodeURIComponent(to.path)}`); + } + + if (!user.value.email_verified) { + return navigateTo('/verify-email'); + } +}); +``` + +## Error Pages + +### 401 Unauthorized Page + +```vue + + +``` + +### 403 Forbidden Page + +```vue + + +``` + +## Testing Protected Routes + +### Unit Testing Middleware + +```typescript +// middleware/auth.spec.ts +import { describe, it, expect, vi } from 'vitest'; +import { mockNuxtImport } from '@nuxt/test-utils/runtime'; + +mockNuxtImport('useUser', () => { + return () => ({ value: null }); +}); + +mockNuxtImport('navigateTo', () => { + return vi.fn((path) => path); +}); + +describe('auth middleware', () => { + it('should redirect to login when user is not authenticated', async () => { + const { default: authMiddleware } = await import('./auth'); + const result = authMiddleware( + { path: '/dashboard' }, + { path: '/' } + ); + + expect(result).toBe('/auth/login?returnTo=/dashboard'); + }); +}); +``` + +### E2E Testing with Playwright + +```typescript +// test/protected-routes.spec.ts +import { test, expect } from '@playwright/test'; + +test('should redirect to login when accessing protected route', async ({ page }) => { + await page.goto('/dashboard'); + await expect(page).toHaveURL(/\/auth\/login/); +}); + +test('should access protected route when logged in', async ({ page }) => { + // Login first + await page.goto('/auth/login'); + // ... perform login + await page.waitForURL('/'); + + // Access protected route + await page.goto('/dashboard'); + await expect(page).toHaveURL('/dashboard'); +}); +``` + +## Security Checklist + +- [ ] Never rely on `useUser()` for any security-critical decisions (it's client-side only and can be tampered with) +- [ ] Always validate sessions server-side with `useAuth0(event).getSession()` for all security-critical decisions +- [ ] Implement proper error handling (401/403 responses) +- [ ] Validate `returnTo` parameter (SDK does this automatically) +- [ ] Use HTTPS in production +- [ ] Implement rate limiting for API routes +- [ ] Log authentication failures for monitoring +- [ ] Test protected routes thoroughly +- [ ] Implement proper error pages +- [ ] Consider role/permission hierarchies + +## Common Pitfalls + +### ❌ Client-Side Only Protection +```typescript +// BAD - Can be bypassed +if (!useUser().value) { + return navigateTo('/login'); +} +``` + +### ✅ Server-Side Validation +```typescript +// GOOD - Cannot be bypassed +const session = await useAuth0(event).getSession(); +if (!session) { + throw createError({ statusCode: 401 }); +} +``` + +### ❌ Forgetting SSR Context +```typescript +// BAD - Only protects client-side navigation +definePageMeta({ middleware: ['auth'] }); +``` + +### ✅ Both Client and Server Protection +```typescript +// GOOD - Protects both client navigation and SSR +definePageMeta({ middleware: ['auth'] }); +// PLUS server middleware in server/middleware/ +``` diff --git a/main/.mintlify/skills/auth0-nuxt/references/session-stores.md b/main/.mintlify/skills/auth0-nuxt/references/session-stores.md new file mode 100644 index 0000000000..26d7582be7 --- /dev/null +++ b/main/.mintlify/skills/auth0-nuxt/references/session-stores.md @@ -0,0 +1,385 @@ +# Custom Session Stores for Auth0-Nuxt + +This guide covers implementing custom session stores for stateful session management in Auth0-Nuxt. + +## When to Use Custom Session Stores + +Use custom session stores when: +- Session data exceeds cookie size limits (4KB per chunk) +- Running in distributed/load-balanced environments +- Storing sensitive PII that shouldn't be in cookies +- Need centralized session management across services +- Implementing advanced session features (expiration, revocation) + +## Stateless vs Stateful Sessions + +### Stateless (Default) +- **Storage**: Encrypted, chunked cookies +- **Advantages**: Simple, no infrastructure, scales horizontally +- **Disadvantages**: 4KB size limit per chunk, data in browser +- **Use When**: Sessions are small, simple deployments + +### Stateful (Custom Store) +- **Storage**: Redis, MongoDB, PostgreSQL, etc. +- **Advantages**: Unlimited size, centralized control, revocable +- **Disadvantages**: Requires infrastructure, network latency +- **Use When**: Large sessions, distributed systems, compliance requirements + +## SessionStore Interface + +All custom stores must implement this interface: + +```typescript +interface SessionStore { + set(identifier: string, stateData: StateData): Promise; + get(identifier: string): Promise; + delete(identifier: string): Promise; + deleteByLogoutToken(claims: any, options?: StoreOptions): Promise; +} +``` + +## Redis Session Store + +Complete implementation using Nitro's unstorage layer: + +### 1. Create Session Store Factory + +```typescript +// server/utils/session-store-factory.ts +import type { SessionStore, StateData, StoreOptions } from '@auth0/auth0-nuxt'; +import type { Storage } from 'unstorage'; + +export class RedisSessionStore implements SessionStore { + readonly #store: Storage; + + constructor(store: Storage) { + this.#store = store; + } + + async set(identifier: string, stateData: StateData): Promise { + await this.#store.setItem(identifier, stateData); + } + + async get(identifier: string): Promise { + const result = await this.#store.getItem(identifier); + // Redis returns null for missing keys, map to undefined + return result ?? undefined; + } + + async delete(identifier: string): Promise { + await this.#store.removeItem(identifier); + } + + async deleteByLogoutToken(claims: any, options?: StoreOptions): Promise { + // Extract session ID from logout token claims + const sid = claims.sid; + + if (sid) { + // Delete session by session ID + await this.delete(sid); + } + } +} + +export default function getSessionStoreInstance() { + const storage = useStorage('redis'); + return new RedisSessionStore(storage); +} +``` + +### 2. Configure Module + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + modules: [ + ['@auth0/auth0-nuxt', { + sessionStoreFactoryPath: '~/server/utils/session-store-factory.ts' + }] + ], + runtimeConfig: { + auth0: { + domain: '', + clientId: '', + clientSecret: '', + sessionSecret: '', + appBaseUrl: 'http://localhost:3000', + }, + }, +}); +``` + +### 3. Configure Nitro Storage + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + nitro: { + storage: { + redis: { + driver: 'redis', + host: process.env.REDIS_HOST || '127.0.0.1', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD, + db: parseInt(process.env.REDIS_DB || '0'), + } + } + } +}); +``` + +### 4. Docker Compose for Local Development + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:7-alpine + ports: + - '6379:6379' + volumes: + - redis_data:/data + +volumes: + redis_data: +``` + +## MongoDB Session Store + +Implementation using Nitro's MongoDB driver: + +```typescript +// server/utils/session-store-factory.ts +import type { SessionStore, StateData, StoreOptions } from '@auth0/auth0-nuxt'; +import type { Storage } from 'unstorage'; + +export class MongoSessionStore implements SessionStore { + readonly #store: Storage; + + constructor(store: Storage) { + this.#store = store; + } + + async set(identifier: string, stateData: StateData): Promise { + await this.#store.setItem(identifier, stateData); + } + + async get(identifier: string): Promise { + const result = await this.#store.getItem(identifier); + return result ?? undefined; + } + + async delete(identifier: string): Promise { + await this.#store.removeItem(identifier); + } + + async deleteByLogoutToken(claims: any, options?: StoreOptions): Promise { + const sid = claims.sid; + if (sid) { + await this.delete(sid); + } + } +} + +export default function getSessionStoreInstance() { + const storage = useStorage('mongodb'); + return new MongoSessionStore(storage); +} +``` + +### MongoDB Nitro Configuration + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + nitro: { + storage: { + mongodb: { + driver: 'mongodb', + connectionString: process.env.MONGODB_URI || 'mongodb://localhost:27017', + databaseName: 'auth0_sessions', + collectionName: 'sessions', + } + } + } +}); +``` + +## PostgreSQL Session Store + +Using a custom implementation with pg library: + +```typescript +// server/utils/session-store-factory.ts +import type { SessionStore, StateData, StoreOptions } from '@auth0/auth0-nuxt'; +import { Pool } from 'pg'; + +export class PostgresSessionStore implements SessionStore { + readonly #pool: Pool; + + constructor() { + this.#pool = new Pool({ + host: process.env.POSTGRES_HOST || 'localhost', + port: parseInt(process.env.POSTGRES_PORT || '5432'), + database: process.env.POSTGRES_DB || 'auth0_sessions', + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + }); + } + + async set(identifier: string, stateData: StateData): Promise { + const query = ` + INSERT INTO sessions (id, data, expires_at) + VALUES ($1, $2, NOW() + INTERVAL '1 day') + ON CONFLICT (id) DO UPDATE SET data = $2, expires_at = NOW() + INTERVAL '1 day' + `; + await this.#pool.query(query, [identifier, JSON.stringify(stateData)]); + } + + async get(identifier: string): Promise { + const query = ` + SELECT data FROM sessions + WHERE id = $1 AND expires_at > NOW() + `; + const result = await this.#pool.query(query, [identifier]); + + if (result.rows.length === 0) { + return undefined; + } + + return JSON.parse(result.rows[0].data); + } + + async delete(identifier: string): Promise { + await this.#pool.query('DELETE FROM sessions WHERE id = $1', [identifier]); + } + + async deleteByLogoutToken(claims: any, options?: StoreOptions): Promise { + const sid = claims.sid; + if (sid) { + await this.delete(sid); + } + } +} + +export default function getSessionStoreInstance() { + return new PostgresSessionStore(); +} +``` + +### PostgreSQL Schema + +```sql +CREATE TABLE sessions ( + id VARCHAR(255) PRIMARY KEY, + data JSONB NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); +``` + +## Back-Channel Logout Implementation + +Proper implementation of `deleteByLogoutToken`: + +```typescript +async deleteByLogoutToken(claims: any, options?: StoreOptions): Promise { + // Claims from the logout_token JWT + const sid = claims.sid; // Session ID + const sub = claims.sub; // User ID + + if (sid) { + // Delete specific session by session ID + await this.delete(sid); + } else if (sub) { + // Delete all sessions for the user + // Implementation depends on your storage + // This example assumes you track sessions by user + await this.deleteAllForUser(sub); + } +} +``` + +## Session Expiration + +Implement TTL (Time-To-Live) in your session store: + +```typescript +// Redis with TTL +async set(identifier: string, stateData: StateData): Promise { + await this.#store.setItem(identifier, stateData, { + ttl: 86400, // 24 hours in seconds + }); +} + +// PostgreSQL with automatic cleanup +// Run this periodically (cronjob or Nitro task) +async cleanup(): Promise { + await this.#pool.query('DELETE FROM sessions WHERE expires_at < NOW()'); +} +``` + +## Testing Your Session Store + +```typescript +// test/session-store.test.ts +import { describe, it, expect } from 'vitest'; +import getSessionStoreInstance from '~/server/utils/session-store-factory'; + +describe('Session Store', () => { + const store = getSessionStoreInstance(); + const testData = { + user: { sub: 'test-user' }, + idToken: 'test-token', + tokenSets: [], + internal: { sid: 'test-session', createdAt: Date.now() }, + }; + + it('should store and retrieve session', async () => { + await store.set('test-id', testData); + const result = await store.get('test-id'); + expect(result).toEqual(testData); + }); + + it('should delete session', async () => { + await store.set('test-id-2', testData); + await store.delete('test-id-2'); + const result = await store.get('test-id-2'); + expect(result).toBeUndefined(); + }); + + it('should handle logout token', async () => { + await store.set('test-id-3', testData); + await store.deleteByLogoutToken({ sid: 'test-session' }); + const result = await store.get('test-id-3'); + expect(result).toBeUndefined(); + }); +}); +``` + +## Performance Considerations + +1. **Connection Pooling**: Always use connection pools for database connections +2. **Caching**: Consider caching frequently accessed sessions +3. **Indexing**: Add indexes on session ID and expiration columns +4. **TTL**: Implement automatic expiration to prevent storage bloat +5. **Serialization**: Use efficient serialization (JSON, MessagePack) + +## Security Considerations + +1. **Encryption**: Session data is already encrypted by Auth0-Nuxt +2. **Access Control**: Restrict database access to your application only +3. **Network Security**: Use TLS for database connections +4. **Secrets Management**: Store credentials in environment variables +5. **Audit Logging**: Log session access for compliance + +## Migration from Stateless to Stateful + +1. Deploy stateful configuration alongside stateless +2. New sessions use stateful store +3. Old cookie-based sessions continue working +4. Gradually phase out cookie sessions as they expire +5. Monitor cookie size metrics diff --git a/main/.mintlify/skills/auth0-quickstart/SKILL.md b/main/.mintlify/skills/auth0-quickstart/SKILL.md index 48e1b50f97..aed82bbe76 100644 --- a/main/.mintlify/skills/auth0-quickstart/SKILL.md +++ b/main/.mintlify/skills/auth0-quickstart/SKILL.md @@ -1,9 +1,25 @@ --- name: auth0-quickstart -description: Use when adding authentication or login to any app - detects your stack (React, Next.js, Vue, Nuxt, Angular, Express, Fastify, React Native), sets up an Auth0 account if needed, and routes to the correct SDK setup workflow. +description: Use when adding authentication or login to any app - detects your stack (React, Next.js, Vue, Nuxt, Angular, Express, Fastify, FastAPI, ASP.NET Core, React Native, Expo, Android, Swift), sets up an Auth0 account if needed, and routes to the correct SDK setup workflow. license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + 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 Quickstart @@ -17,11 +33,11 @@ Detect your framework and get started with Auth0 authentication. **Run this command to identify your framework:** ```bash -# Check package.json dependencies -cat package.json | grep -E "react|next|vue|nuxt|angular|express|fastify|@nestjs" +# Check package.json dependencies (Node.js projects) +cat package.json | grep -E "react|next|vue|nuxt|angular|express|fastify|@nestjs|expo" # Or check project files -ls -la | grep -E "angular.json|vue.config.js|next.config" +ls -la | grep -E "angular.json|vue.config.js|next.config|app.json|Package.swift|build.gradle" ``` **Framework Detection Table:** @@ -37,6 +53,8 @@ ls -la | grep -E "angular.json|vue.config.js|next.config" | Fastify (web app) | `"fastify"` in package.json, has `@fastify/view` | `auth0-fastify` | | Fastify (API) | `"fastify"` in package.json, no view engine | `auth0-fastify-api` | | React Native | `"react-native"` or `"expo"` in package.json | `auth0-react-native` | +| Flask | `"flask"` in requirements.txt, Pipfile, or pyproject.toml | `auth0-flask` | +| Node.js API | `"express-oauth2-jwt-bearer"` in package.json | `express-oauth2-jwt-bearer` | **Don't see your framework?** See Tier 2 Frameworks below. @@ -76,6 +94,7 @@ Choose application type based on your framework: **Single Page Applications (React, Vue, Angular):** ```bash auth0 apps create --name "My App" --type spa \ + --auth-method None \ --callbacks "http://localhost:3000" \ --logout-urls "http://localhost:3000" \ --metadata "created_by=agent_skills" @@ -92,6 +111,7 @@ auth0 apps create --name "My App" --type regular \ **Native Apps (React Native):** ```bash auth0 apps create --name "My App" --type native \ + --auth-method None \ --callbacks "myapp://callback" \ --logout-urls "myapp://logout" \ --metadata "created_by=agent_skills" @@ -105,6 +125,20 @@ auth0 apps show # Get client ID and secret **More CLI commands:** See [CLI Reference](references/cli.md) +### Apply Branding (Optional) + +After creating your application, apply branding so the Auth0 Universal Login page matches your app: + +```bash +auth0 ul update \ + --accent "#YOUR_BRAND_COLOR" \ + --background "#YOUR_BACKGROUND_COLOR" \ + --logo "https://your-app.com/logo.png" \ + --favicon "https://your-app.com/favicon.ico" +``` + +This ensures users see your app's branding on the login screen instead of the default Auth0 branding. You can also use the `acul-screen-generator` skill for full custom login screen design. + --- ## Step 4: Use Framework-Specific Skill @@ -122,8 +156,10 @@ Based on your framework detection, use the appropriate skill: **Backend:** - **`auth0-express`** - Express.js web applications +- **`auth0-flask`** - Flask web applications - **`auth0-fastify`** - Fastify web applications - **`auth0-fastify-api`** - Fastify API authentication +- **`express-oauth2-jwt-bearer`** - Node.js/Express API JWT Bearer validation **Mobile:** - **`auth0-react-native`** - React Native and Expo (iOS/Android) @@ -137,7 +173,6 @@ Not yet available as separate skills. Use Auth0 documentation: - [Remix](https://auth0.com/docs/quickstart/webapp/remix) **Backend:** -- [Flask (Python)](https://auth0.com/docs/quickstart/webapp/python) - [FastAPI (Python)](https://auth0.com/docs/quickstart/backend/python) - [Django (Python)](https://auth0.com/docs/quickstart/webapp/django) - [Rails (Ruby)](https://auth0.com/docs/quickstart/webapp/rails) @@ -211,15 +246,23 @@ Complete Auth0 CLI reference: - `auth0-migration` - Migrate from other auth providers ### SDK Skills +- `auth0-spa-js` - SPA integration - `auth0-react` - React SPA integration - `auth0-nextjs` - Next.js integration - `auth0-vue` - Vue.js integration - `auth0-nuxt` - Nuxt 3/4 integration - `auth0-angular` - Angular integration - `auth0-express` - Express.js integration +- `auth0-flask` - Flask web app integration - `auth0-fastify` - Fastify web app integration - `auth0-fastify-api` - Fastify API integration -- `auth0-react-native` - React Native/Expo integration +- `express-oauth2-jwt-bearer` - Node.js/Express API JWT Bearer validation +- `auth0-react-native` - React Native CLI (bare workflow) integration +- `auth0-expo` - Expo (managed workflow) integration +- `auth0-android` - Android (Kotlin/Java) integration +- `auth0-swift` - iOS/macOS (Swift) integration +- `auth0-fastapi-api` - FastAPI API authentication +- `auth0-aspnetcore-api` - ASP.NET Core API authentication ### Advanced Features - `auth0-mfa` - Multi-Factor Authentication diff --git a/main/.mintlify/skills/auth0-quickstart/references/cli.md b/main/.mintlify/skills/auth0-quickstart/references/cli.md new file mode 100644 index 0000000000..5bb4b8d4b0 --- /dev/null +++ b/main/.mintlify/skills/auth0-quickstart/references/cli.md @@ -0,0 +1,338 @@ +# Auth0 CLI Reference + +Complete guide to installing, configuring, and using the Auth0 CLI. + +--- + +## Installation + +### macOS/Linux + +**Via Homebrew (recommended):** +```bash +brew install auth0/auth0-cli/auth0 +``` + +**Via curl (review script before executing):** +```bash +curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh +# Review the script before running: cat /tmp/auth0-install.sh +sh /tmp/auth0-install.sh +rm /tmp/auth0-install.sh +``` + +### Windows + +**Via Scoop:** +```bash +scoop install auth0 +``` + +**Via Chocolatey:** +```bash +choco install auth0-cli +``` + +### Verify Installation + +```bash +auth0 --version +which auth0 +``` + +--- + +## Initial Setup + +### Login to Auth0 + +```bash +auth0 login +``` + +This opens your browser to authenticate with Auth0. + +### Check Current Tenant + +```bash +# List all tenants +auth0 tenants list + +# Show current tenant +auth0 tenants use +``` + +--- + +## Creating Applications + +### Single Page Application (SPA) + +For React, Vue, Angular applications: + +```bash +auth0 apps create \ + --name "My React App" \ + --type spa \ + --callbacks "http://localhost:3000" \ + --logout-urls "http://localhost:3000" \ + --origins "http://localhost:3000" \ + --web-origins "http://localhost:3000" \ + --metadata "created_by=agent_skills" +``` + +### Regular Web Application + +For Next.js, Express, server-rendered applications: + +```bash +auth0 apps create \ + --name "My Next.js App" \ + --type regular \ + --callbacks "http://localhost:3000/api/auth/callback" \ + --logout-urls "http://localhost:3000" \ + --metadata "created_by=agent_skills" +``` + +### Native Application + +For React Native, iOS, Android, mobile applications: + +```bash +auth0 apps create \ + --name "My Mobile App" \ + --type native \ + --callbacks "myapp://callback" \ + --logout-urls "myapp://logout" \ + --metadata "created_by=agent_skills" +``` + +### Machine-to-Machine (M2M) + +For backend APIs, cron jobs, server-to-server: + +```bash +auth0 apps create \ + --name "My API Service" \ + --type m2m \ + --metadata "created_by=agent_skills" +``` + +--- + +## Managing Applications + +### List Applications + +```bash +# List all applications +auth0 apps list +``` + +### Show Application Details + +```bash +# Show app details (includes client ID and secret) +auth0 apps show +``` + +### Update Application + +```bash +# Update callbacks +auth0 apps update \ + --callbacks "http://localhost:3000,https://myapp.com" + +# Update logout URLs +auth0 apps update \ + --logout-urls "http://localhost:3000,https://myapp.com" +``` + +### Delete Application + +```bash +auth0 apps delete +``` + +--- + +## User Management + +### List Users + +```bash +# List all users +auth0 users list + +# List with search +auth0 users search --query "email:*@example.com" +``` + +### Create User + +```bash +auth0 users create \ + --email "user@example.com" \ + --password "SecurePassword123!" \ + --connection "Username-Password-Authentication" +``` + +### Show User Details + +```bash +auth0 users show +``` + +### Delete User + +```bash +auth0 users delete +``` + +--- + +## Testing & Debugging + +### Test Login Flow + +```bash +# Test login with Universal Login +auth0 test login +``` + +### Get Access Token + +```bash +# Get access token for testing APIs +auth0 test token +``` + +### Live Log Streaming + +```bash +# Tail Auth0 logs in real-time +auth0 logs tail + +# Filter by type +auth0 logs tail --filter "type:f" # Failed logins +auth0 logs tail --filter "type:s" # Successful logins +``` + +--- + +## API Management + +### List APIs + +```bash +auth0 apis list +``` + +### Create API + +```bash +auth0 apis create \ + --name "My API" \ + --identifier "https://api.myapp.com" +``` + +### Show API Details + +```bash +auth0 apis show +``` + +--- + +## Command Quick Reference + +### Account & Tenant + +| Command | Purpose | +|---------|---------| +| `auth0 login` | Login to Auth0 | +| `auth0 logout` | Logout from Auth0 | +| `auth0 tenants list` | List your Auth0 tenants | +| `auth0 tenants use ` | Switch to a different tenant | + +### Applications + +| Command | Purpose | +|---------|---------| +| `auth0 apps list` | List all applications | +| `auth0 apps show ` | Show application details (get credentials) | +| `auth0 apps create` | Create a new application | +| `auth0 apps update ` | Update application settings | +| `auth0 apps delete ` | Delete application | +| `auth0 apps open ` | Open application in dashboard | + +### Users + +| Command | Purpose | +|---------|---------| +| `auth0 users list` | List users in tenant | +| `auth0 users show ` | Show user details | +| `auth0 users create` | Create a new user | +| `auth0 users delete ` | Delete user | +| `auth0 users search` | Search users by query | +| `auth0 users open ` | Open user in dashboard | + +### APIs + +| Command | Purpose | +|---------|---------| +| `auth0 apis list` | List all APIs | +| `auth0 apis show ` | Show API details | +| `auth0 apis create` | Create a new API | +| `auth0 apis delete ` | Delete API | +| `auth0 apis open ` | Open API in dashboard | + +### Testing & Debugging + +| Command | Purpose | +|---------|---------| +| `auth0 test login ` | Test login flow | +| `auth0 test token ` | Get access token for testing | +| `auth0 logs tail` | Live tail of Auth0 logs | +| `auth0 logs list` | List recent log entries | + +### Utility + +| Command | Purpose | +|---------|---------| +| `auth0 --version` | Show CLI version | +| `auth0 --help` | Show help for any command | +| `auth0 completion` | Generate shell completion | + +--- + +## Common Flags + +### Global Flags + +- `--json` - Output as JSON +- `--format ` - Output format (json, yaml, csv) +- `--tenant ` - Specify tenant +- `--debug` - Enable debug logging +- `--no-color` - Disable colored output + +### Examples + +```bash +# Get JSON output +auth0 apps list --json + +# Use specific tenant +auth0 apps list --tenant my-tenant + +# Debug mode +auth0 apps create --debug --name "My App" --type spa --auth-method None +``` + +--- + +## References + +- [Auth0 CLI GitHub](https://github.com/auth0/auth0-cli) +- [Auth0 CLI Documentation](https://auth0.github.io/auth0-cli/) +- [Auth0 Management API](https://auth0.com/docs/api/management/v2) diff --git a/main/.mintlify/skills/auth0-quickstart/references/concepts.md b/main/.mintlify/skills/auth0-quickstart/references/concepts.md new file mode 100644 index 0000000000..df000feac3 --- /dev/null +++ b/main/.mintlify/skills/auth0-quickstart/references/concepts.md @@ -0,0 +1,380 @@ +# Auth0 Concepts & Troubleshooting + +Core Auth0 concepts, terminology, troubleshooting guide, and security best practices. + +--- + +## Application Types + +Understanding which application type to choose: + +| Type | Use Case | Client Secret | PKCE | Token Storage | +|------|----------|---------------|------|---------------| +| **Single Page Application (SPA)** | React, Vue, Angular, client-side apps | ❌ Not used | ✅ Required | Browser (memory/storage) | +| **Regular Web Application** | Next.js, Express, Rails, server-rendered | ✅ Required | ✅ Recommended | Server-side session | +| **Native Application** | React Native, iOS, Android, Electron | ❌ Not used | ✅ Required | Secure device storage | +| **Machine-to-Machine (M2M)** | Backend APIs, cron jobs, services | ✅ Required | ❌ Not applicable | Server environment | + +### When to Use Each Type + +**Single Page Application (SPA):** +- Client-side only (no backend server) +- JavaScript runs in browser +- Cannot keep secrets secure +- Examples: Vite React app, Vue CLI app + +**Regular Web Application:** +- Has a backend server +- Server renders pages or handles authentication +- Can securely store secrets +- Examples: Next.js app, Express with sessions + +**Native Application:** +- Mobile or desktop application +- Runs on user's device +- Cannot keep secrets secure (can be extracted) +- Examples: React Native app, iOS app, Electron app + +**Machine-to-Machine (M2M):** +- No user interaction +- Server-to-server communication +- Uses Client Credentials flow +- Examples: Cron jobs, backend services, APIs calling APIs + +--- + +## Key Terms + +### Authentication Terms + +| Term | Definition | Example | +|------|------------|---------| +| **Domain** | Your Auth0 tenant URL | `your-tenant.auth0.com` | +| **Client ID** | Public identifier for your app | `abc123xyz` (safe to expose) | +| **Client Secret** | Confidential secret (server-side only) | Never expose in client code! | +| **Tenant** | Your Auth0 account/instance | `your-tenant` | +| **Connection** | Authentication source | Username-Password, Google, SAML | +| **Universal Login** | Auth0's hosted login page | Recommended for best security | + +### URLs and Redirects + +| Term | Definition | Example | +|------|------------|---------| +| **Callback URL** | Where Auth0 redirects after login | `http://localhost:3000/callback` | +| **Logout URL** | Where Auth0 redirects after logout | `http://localhost:3000` | +| **Allowed Origins** | Domains that can make requests | `http://localhost:3000` | +| **Allowed Web Origins** | For CORS and silent auth | `http://localhost:3000` | + +### Tokens + +| Token | Purpose | Lifespan | Where Stored | +|-------|---------|----------|--------------| +| **Access Token** | API authorization | Short (hours) | Client or server | +| **ID Token** | User identity info (JWT) | Short (hours) | Client or server | +| **Refresh Token** | Get new access tokens | Long (days/months) | Secure storage only | + +### OAuth/OIDC Terms + +| Term | Definition | +|------|------------| +| **Audience** | API identifier, specifies which API the token is for | +| **Scope** | Permissions requested (e.g., `openid profile email`) | +| **PKCE** | Proof Key for Code Exchange - security for SPAs/mobile | +| **Grant Type** | OAuth flow type (Authorization Code, Client Credentials, etc.) | +| **Claims** | Information in a token (email, name, roles, etc.) | + +--- + +## OAuth Flows + +### Authorization Code Flow with PKCE + +**Used by:** SPAs, Native apps + +**How it works:** +1. App redirects to Auth0 login +2. User authenticates +3. Auth0 redirects back with authorization code +4. App exchanges code for tokens (with PKCE proof) + +**Security:** PKCE prevents authorization code interception + +### Authorization Code Flow (with Secret) + +**Used by:** Regular web applications (server-side) + +**How it works:** +1. App redirects to Auth0 login +2. User authenticates +3. Auth0 redirects back with authorization code +4. Server exchanges code for tokens using client secret + +**Security:** Client secret never exposed to browser + +### Client Credentials Flow + +**Used by:** M2M applications + +**How it works:** +1. Service authenticates with client ID + secret +2. Receives access token +3. Uses token to call APIs + +**Security:** No user involved, service-to-service only + +--- + +## Troubleshooting + +### Common Issues + +| Issue | Symptom | Solution | +|-------|---------|----------| +| **Invalid callback URL** | Error after login redirect | Add callback URL in Auth0 dashboard app settings | +| **Invalid state** | Error during login | Clear browser storage and cookies, try again | +| **Client authentication failed** | 401 error | Verify client ID and secret are correct | +| **Missing required parameter** | Auth0 error message | Check environment variables are loaded | +| **Unauthorized when calling API** | 401 from API | Ensure `audience` is configured in auth config | +| **Session not created** | Login works but no session | Check `AUTH0_SECRET` is set (server-side apps) | +| **CORS errors** | Browser blocks requests | Add your domain to Allowed Web Origins | +| **Token expired** | 401 after some time | Implement token refresh or re-authentication | + +### Debugging Steps + +1. **Check Auth0 Logs** + - Go to Auth0 Dashboard → Monitoring → Logs + - Or use: `auth0 logs tail` + +2. **Verify Environment Variables** + ```bash + # Check variables are loaded + console.log(process.env.AUTH0_DOMAIN) + console.log(import.meta.env.VITE_AUTH0_DOMAIN) + ``` + +3. **Inspect Network Traffic** + - Open browser DevTools → Network tab + - Look for failed auth requests + - Check response errors + +4. **Clear Auth State** + ```bash + # Clear browser storage + localStorage.clear() + sessionStorage.clear() + # Clear cookies for your domain + ``` + +5. **Test with Auth0 CLI** + ```bash + # Test login flow + auth0 test login + + # Get test token + auth0 test token + ``` + +--- + +## Security Best Practices + +### ✅ Recommended Practices + +1. **Use HTTPS in Production** + - Auth0 requires HTTPS callback URLs + - Never use HTTP in production + +2. **Never Commit Secrets** + - Add `.env` files to `.gitignore` + - Use secret management tools (AWS Secrets Manager, etc.) + +3. **Rotate Secrets Regularly** + - Update `AUTH0_SECRET` every 90 days + - Rotate client secrets periodically + +4. **Use PKCE** + - Required for SPAs and native apps + - Enabled by default in modern Auth0 SDKs + +5. **Validate Tokens on Backend** + - Never trust client-side validation + - Always verify JWT signature server-side + +6. **Configure CORS Properly** + - Only add your actual domains to Allowed Origins + - Don't use wildcards (`*`) in production + +7. **Use Refresh Tokens Wisely** + - Enable refresh token rotation + - Store securely (not in localStorage) + +8. **Implement Rate Limiting** + - Protect Auth0 endpoints from abuse + - Use Auth0's anomaly detection + +9. **Enable MFA for Admin Accounts** + - Require MFA for Auth0 dashboard access + - Use hardware security keys when possible + +10. **Monitor Auth0 Logs** + - Set up log streaming to SIEM + - Alert on suspicious activity + +### ❌ Security Anti-Patterns + +1. **Hardcoding Credentials** + ```javascript + // ❌ NEVER DO THIS + const clientSecret = "abc123secret"; + ``` + +2. **Exposing Client Secret in Frontend** + ```javascript + // ❌ NEVER in browser code + clientSecret: process.env.CLIENT_SECRET + ``` + +3. **Using Client Secret in SPAs** + - SPAs cannot keep secrets + - Use SPA application type instead + +4. **Storing Tokens in localStorage (Sensitive Apps)** + - XSS can steal tokens + - Consider in-memory storage or httpOnly cookies + +5. **Skipping Token Validation** + ```javascript + // ❌ Don't trust tokens blindly + const user = jwt.decode(token); // No validation! + + // ✅ Always verify + const user = jwt.verify(token, publicKey, options); + ``` + +6. **Using HTTP in Production** + - Tokens sent over HTTP can be intercepted + - Always use HTTPS + +--- + +## Common Auth0 Configurations + +### Recommended Scopes + +**For user authentication:** +``` +openid profile email +``` + +**For API access:** +``` +openid profile email read:users write:users +``` + +**Minimal:** +``` +openid +``` + +### Token Lifespans + +**Recommended values:** + +| Token Type | Development | Production | +|------------|-------------|------------| +| Access Token | 1 hour | 1 hour | +| ID Token | 1 hour | 1 hour | +| Refresh Token | 30 days | 30 days | + +**Note:** Shorter is more secure, but requires more frequent renewal. + +### Session Configuration + +**Express.js example:** +```javascript +app.use(auth({ + secret: process.env.AUTH0_SECRET, + session: { + rolling: true, // Extend session on activity + rollingDuration: 86400, // 24 hours + absoluteDuration: 604800 // 7 days max + } +})); +``` + +--- + +## Migrating Between Application Types + +### From SPA to Regular Web App + +**Why:** Need server-side rendering, better security for secrets + +**Changes needed:** +1. Create new Auth0 app (type: Regular Web App) +2. Add client secret to environment (server-side only) +3. Change callback URL pattern (usually `/api/auth/callback`) +4. Update SDK (e.g., from `auth0-react` to `nextjs-auth0`) +5. Move auth logic to server + +### From Regular Web App to SPA + +**Why:** Want client-side only, serverless deployment + +**Changes needed:** +1. Create new Auth0 app (type: SPA) +2. Remove client secret (not used in SPAs) +3. Enable PKCE +4. Update callback URLs (no `/api/auth/` prefix) +5. Update SDK to client-side SDK + +--- + +## Advanced Concepts + +### Organizations (B2B) + +Multi-tenant B2B applications: +- Separate login domains per organization +- Organization-specific branding +- Organization invitation flows + +**Learn more:** `auth0-organizations` skill + +### Multi-Factor Authentication (MFA) + +Require additional verification: +- TOTP (Google Authenticator) +- SMS, Email, Push +- WebAuthn (security keys, biometrics) +- Step-up authentication for sensitive operations + +**Learn more:** `auth0-mfa` skill + +### Passwordless Authentication + +Login without passwords: +- Magic links via email +- SMS one-time codes +- WebAuthn / Passkeys + +**Learn more:** `auth0-passkeys` skill + +### Custom Domains + +Use your own domain for Auth0: +- `auth.myapp.com` instead of `tenant.auth0.com` +- Better branding +- First-party cookies + +**Setup:** Auth0 Dashboard → Branding → Custom Domains + +--- + +## References + +- [Auth0 Docs - Application Settings](https://auth0.com/docs/get-started/applications) +- [Auth0 Docs - Tokens](https://auth0.com/docs/secure/tokens) +- [OAuth 2.0 Specification](https://oauth.net/2/) +- [OpenID Connect Specification](https://openid.net/connect/) +- [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636) diff --git a/main/.mintlify/skills/auth0-quickstart/references/environments.md b/main/.mintlify/skills/auth0-quickstart/references/environments.md new file mode 100644 index 0000000000..d45fa25061 --- /dev/null +++ b/main/.mintlify/skills/auth0-quickstart/references/environments.md @@ -0,0 +1,380 @@ +# Environment Variables Reference + +Environment variable configuration for Auth0 across different frameworks and application types. + +--- + +## Single Page Applications (SPAs) + +### Vite (React, Vue, Svelte) + +Create `.env`: + +```bash +VITE_AUTH0_DOMAIN=.auth0.com +VITE_AUTH0_CLIENT_ID= +``` + +**Usage in code:** +```javascript +const domain = import.meta.env.VITE_AUTH0_DOMAIN; +const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; +``` + +**Important:** +- Vite requires `VITE_` prefix +- Use `import.meta.env`, NOT `process.env` +- Restart dev server after changing `.env` + +--- + +### Create React App + +Create `.env`: + +```bash +REACT_APP_AUTH0_DOMAIN=.auth0.com +REACT_APP_AUTH0_CLIENT_ID= +``` + +**Usage in code:** +```javascript +const domain = process.env.REACT_APP_AUTH0_DOMAIN; +const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID; +``` + +**Important:** +- Create React App requires `REACT_APP_` prefix +- Use `process.env` +- Restart dev server after changing `.env` + +--- + +### Angular + +Update `src/environments/environment.ts`: + +```typescript +export const environment = { + production: false, + auth0: { + domain: '.auth0.com', + clientId: '', + authorizationParams: { + redirect_uri: window.location.origin + } + } +}; +``` + +Update `src/environments/environment.prod.ts`: + +```typescript +export const environment = { + production: true, + auth0: { + domain: '.auth0.com', + clientId: '', + authorizationParams: { + redirect_uri: 'https://myapp.com' + } + } +}; +``` + +**Usage in code:** +```typescript +import { environment } from '../environments/environment'; + +const domain = environment.auth0.domain; +``` + +--- + +## Server-Side Applications + +### Next.js + +Create `.env.local`: + +```bash +AUTH0_SECRET='' +AUTH0_BASE_URL='http://localhost:3000' +AUTH0_ISSUER_BASE_URL='https://.auth0.com' +AUTH0_CLIENT_ID='' +AUTH0_CLIENT_SECRET='' +``` + +**Generate AUTH0_SECRET:** +```bash +openssl rand -hex 32 +``` + +**Usage in code:** +```javascript +// Automatic via @auth0/nextjs-auth0 +// No manual import needed +``` + +**Important:** +- File must be named `.env.local` (not `.env`) +- Add `.env.local` to `.gitignore` +- `AUTH0_SECRET` must be 32+ characters +- In production, set these as environment variables in your hosting platform + +**Production (.env.production):** +```bash +AUTH0_SECRET='' +AUTH0_BASE_URL='https://myapp.com' +AUTH0_ISSUER_BASE_URL='https://.auth0.com' +AUTH0_CLIENT_ID='' +AUTH0_CLIENT_SECRET='' +``` + +--- + +### Express.js + +Create `.env`: + +```bash +AUTH0_SECRET='' +AUTH0_BASE_URL='http://localhost:3000' +AUTH0_ISSUER_BASE_URL='https://.auth0.com' +AUTH0_CLIENT_ID='' +AUTH0_CLIENT_SECRET='' +``` + +**Usage in code:** +```javascript +require('dotenv').config(); + +const { auth } = require('express-openid-connect'); + +app.use(auth({ + secret: process.env.AUTH0_SECRET, + baseURL: process.env.AUTH0_BASE_URL, + clientID: process.env.AUTH0_CLIENT_ID, + issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL, + clientSecret: process.env.AUTH0_CLIENT_SECRET +})); +``` + +**Important:** +- Install `dotenv`: `npm install dotenv` +- Add `.env` to `.gitignore` +- Load dotenv at app entry point: `require('dotenv').config()` + +--- + +## Mobile Applications + +### React Native (Expo) + +**Using Expo:** + +Update `app.json`: + +```json +{ + "expo": { + "extra": { + "auth0Domain": ".auth0.com", + "auth0ClientId": "" + } + } +} +``` + +**Usage in code:** +```javascript +import Constants from 'expo-constants'; + +const domain = Constants.expoConfig.extra.auth0Domain; +const clientId = Constants.expoConfig.extra.auth0ClientId; +``` + +--- + +### React Native (CLI) + +Create `.env`: + +```bash +AUTH0_DOMAIN=.auth0.com +AUTH0_CLIENT_ID= +``` + +**Using react-native-config:** + +```bash +npm install react-native-config +``` + +**Usage in code:** +```javascript +import Config from 'react-native-config'; + +const domain = Config.AUTH0_DOMAIN; +const clientId = Config.AUTH0_CLIENT_ID; +``` + +--- + +### Alternative: Config File + +Create `auth0.config.js`: + +```javascript +export default { + domain: '.auth0.com', + clientId: '' +}; +``` + +**Usage:** +```javascript +import auth0Config from './auth0.config'; +``` + +**Important:** +- Add `auth0.config.js` to `.gitignore` +- Create `auth0.config.example.js` as template + +--- + +## API Protection (Backend) + +### Node.js / Express API + +Create `.env`: + +```bash +AUTH0_AUDIENCE='https://api.myapp.com' +AUTH0_ISSUER_BASE_URL='https://.auth0.com' +``` + +**Usage:** +```javascript +const { auth } = require('express-oauth2-jwt-bearer'); + +const checkJwt = auth({ + audience: process.env.AUTH0_AUDIENCE, + issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL +}); + +app.get('/api/protected', checkJwt, (req, res) => { + res.json({ message: 'Protected endpoint' }); +}); +``` + +--- + +### Python / Flask API + +Create `.env`: + +```bash +AUTH0_DOMAIN=.auth0.com +AUTH0_AUDIENCE=https://api.myapp.com +``` + +**Usage:** +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +AUTH0_DOMAIN = os.getenv('AUTH0_DOMAIN') +AUTH0_AUDIENCE = os.getenv('AUTH0_AUDIENCE') +``` + +--- + +## Security Best Practices + +### ✅ DO + +- **Use environment variables** for all secrets +- **Add `.env` files to `.gitignore`** immediately +- **Create `.env.example`** with placeholder values for team +- **Use different secrets** for development and production +- **Rotate secrets regularly** (every 90 days recommended) +- **Use strong AUTH0_SECRET** (32+ random characters) + +### ❌ DON'T + +- **Never commit `.env` files** to version control +- **Never hardcode credentials** in source code +- **Never use same secrets** across environments +- **Never share secrets** via Slack/email/chat +- **Never use weak secrets** like "secret123" + +--- + +## Example `.env.example` + +Create this file to help team members: + +```bash +# Auth0 Configuration +# Get these from: https://manage.auth0.com/dashboard + +AUTH0_SECRET='use-openssl-rand-hex-32-to-generate' +AUTH0_BASE_URL='http://localhost:3000' +AUTH0_ISSUER_BASE_URL='https://YOUR-TENANT.auth0.com' +AUTH0_CLIENT_ID='YOUR-CLIENT-ID' +AUTH0_CLIENT_SECRET='YOUR-CLIENT-SECRET' + +# Optional: API Configuration +AUTH0_AUDIENCE='https://api.myapp.com' +``` + +--- + +## Troubleshooting + +### Variables Not Loading + +**Symptoms:** +- `undefined` when accessing environment variables +- Auth fails with "domain is required" errors + +**Solutions:** + +1. **Restart dev server** after changing `.env` +2. **Check variable prefix** (VITE_, REACT_APP_, etc.) +3. **Verify file name** (.env vs .env.local) +4. **Check file location** (must be in project root) +5. **Load dotenv** for Node.js: `require('dotenv').config()` + +### Wrong Import Method + +**Vite:** +```javascript +// ❌ Wrong +process.env.VITE_AUTH0_DOMAIN + +// ✅ Correct +import.meta.env.VITE_AUTH0_DOMAIN +``` + +**Create React App:** +```javascript +// ❌ Wrong +import.meta.env.REACT_APP_AUTH0_DOMAIN + +// ✅ Correct +process.env.REACT_APP_AUTH0_DOMAIN +``` + +--- + +## References + +- [Vite Environment Variables](https://vitejs.dev/guide/env-and-mode.html) +- [Create React App Environment Variables](https://create-react-app.dev/docs/adding-custom-environment-variables/) +- [Next.js Environment Variables](https://nextjs.org/docs/basic-features/environment-variables) +- [Expo Environment Variables](https://docs.expo.dev/guides/environment-variables/) +- [dotenv Documentation](https://github.com/motdotla/dotenv) diff --git a/main/.mintlify/skills/auth0-react-native/SKILL.md b/main/.mintlify/skills/auth0-react-native/SKILL.md index b07a2df49a..8f687c6662 100644 --- a/main/.mintlify/skills/auth0-react-native/SKILL.md +++ b/main/.mintlify/skills/auth0-react-native/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding authentication to React Native or Expo mobile apps license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 React Native Integration @@ -20,6 +24,7 @@ Add authentication to React Native and Expo mobile applications using react-nati ## When NOT to Use +- **Expo managed workflow** - Use `auth0-expo` skill for Expo apps with config plugin - **React web applications** - Use `auth0-react` skill for SPAs (Vite/CRA) - **React Server Components** - Use `auth0-nextjs` for Next.js applications - **Non-React native apps** - Use platform-specific SDKs (Swift for iOS, Kotlin for Android) @@ -221,6 +226,7 @@ npx react-native run-android - `auth0-quickstart` - Basic Auth0 setup - `auth0-migration` - Migrate from another auth provider - `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-cli` - Manage Auth0 resources from the terminal --- diff --git a/main/.mintlify/skills/auth0-react-native/references/api.md b/main/.mintlify/skills/auth0-react-native/references/api.md new file mode 100644 index 0000000000..6fd77c5204 --- /dev/null +++ b/main/.mintlify/skills/auth0-react-native/references/api.md @@ -0,0 +1,63 @@ +## Testing + +### iOS Testing + +1. Run the app: `npx react-native run-ios` or `npx expo run:ios` +2. Tap "Login" button +3. Safari opens with Auth0 Universal Login +4. Complete authentication +5. App opens via deep link with user authenticated +6. Tap "Logout" and verify session cleared + +### Android Testing + +1. Run the app: `npx react-native run-android` or `npx expo run:android` +2. Tap "Login" button +3. Chrome Custom Tabs opens with Auth0 login +4. Complete authentication +5. App resumes via intent filter with user authenticated +6. Tap "Logout" and verify session cleared + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| Deep link not working (iOS) | Check `CFBundleURLSchemes` matches bundle identifier exactly | +| Deep link not working (Android) | Verify `android:scheme` and `android:host` in AndroidManifest.xml | +| "Invalid state" error | Clear app data and reinstall. Check callback URLs match configuration | +| Login opens but doesn't return to app | Ensure deep linking is properly configured and tested | +| Expo build fails | Run `npx expo prebuild` to generate native configuration | +| iOS builds fail after pod install | Run `cd ios && pod deintegrate && pod install` | + +--- + +## Security Considerations + +- **Use secure storage** - Credentials are stored securely using Keychain (iOS) and Keystore (Android) +- **HTTPS only** - Auth0 requires HTTPS callback URLs (except localhost for dev) +- **Validate tokens on backend** - Never trust client-side token validation +- **Use PKCE** - Enabled by default with react-native-auth0 +- **Implement biometric authentication** - Use react-native-biometrics with Auth0 for enhanced security +- **Handle token expiration** - Implement refresh token logic with `getCredentials()` + +--- + +## Related Skills + +- `auth0-quickstart` - Initial Auth0 account setup +- `auth0-migration` - Migrate from another auth provider +- `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-passkeys` - Add passkey authentication +- `auth0-organizations` - B2B multi-tenancy support + +--- + +## References + +- [React Native Auth0 SDK Documentation](https://auth0.com/docs/libraries/auth0-react-native) +- [React Native Auth0 SDK GitHub](https://github.com/auth0/react-native-auth0) +- [Auth0 React Native Quickstart](https://auth0.com/docs/quickstart/native/react-native) +- [React Native Deep Linking](https://reactnative.dev/docs/linking) +- [Expo Deep Linking](https://docs.expo.dev/guides/deep-linking/) diff --git a/main/.mintlify/skills/auth0-react-native/references/patterns.md b/main/.mintlify/skills/auth0-react-native/references/patterns.md new file mode 100644 index 0000000000..0f92c81591 --- /dev/null +++ b/main/.mintlify/skills/auth0-react-native/references/patterns.md @@ -0,0 +1,193 @@ +## Common Patterns + +### Protected Screen with Navigation + +```tsx +import { useAuth0 } from 'react-native-auth0'; +import { useEffect } from 'react'; +import { NavigationProp } from '@react-navigation/native'; + +export function ProtectedScreen({ navigation }: { navigation: NavigationProp }) { + const { user, isLoading } = useAuth0(); + + useEffect(() => { + if (!isLoading && !user) { + navigation.navigate('Login'); + } + }, [isLoading, user, navigation]); + + if (isLoading) { + return ; + } + + if (!user) { + return null; + } + + return ( + + Protected Content + User ID: {user.sub} + + ); +} +``` + +--- + +### Get User Profile + +```tsx +import { useAuth0 } from 'react-native-auth0'; +import { View, Text, Image } from 'react-native'; + +export function ProfileScreen() { + const { user } = useAuth0(); + + if (!user) { + return Please log in; + } + + return ( + + {user.picture && ( + + )} + Name: {user.name} + Email: {user.email} + Email Verified: {user.email_verified ? 'Yes' : 'No'} + User ID: {user.sub} + + ); +} +``` + +--- + +### Call Protected API + +```tsx +import { useAuth0 } from 'react-native-auth0'; +import { useState } from 'react'; +import { View, Button, Text } from 'react-native'; + +export function ApiTestScreen() { + const { getCredentials } = useAuth0(); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + const callApi = async () => { + try { + const credentials = await getCredentials(); + + const response = await fetch('https://your-api.com/data', { + headers: { + Authorization: `Bearer ${credentials.accessToken}` + } + }); + + const json = await response.json(); + setData(json); + } catch (err) { + setError(err.message); + } + }; + + return ( + + + {error &&
Error: {error}
} + {data &&
{JSON.stringify(data, null, 2)}
} +
+ ); +} +``` + +### Configure Provider for API Calls + +When calling APIs, add `audience` to your Auth0Provider: + +```tsx + + + +``` + +--- + +## Error Handling + +### Handle Loading and Error States + +```tsx +import { useAuth0 } from '@auth0/auth0-react'; + +export function App() { + const { isLoading, error, isAuthenticated, user } = useAuth0(); + + if (isLoading) { + return
Loading authentication...
; + } + + if (error) { + return
Authentication error: {error.message}
; + } + + return isAuthenticated ? ( +
+

Welcome back, {user?.name}!

+ +
+ ) : ( +
+

Please log in

+ +
+ ); +} +``` + +--- + +## Silent Authentication + +### Auto-login on Page Load + +```tsx +import { useAuth0 } from '@auth0/auth0-react'; +import { useEffect } from 'react'; + +export function App() { + const { isAuthenticated, isLoading, getAccessTokenSilently } = useAuth0(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + // Attempt silent authentication + getAccessTokenSilently().catch(() => { + // User not logged in, do nothing + }); + } + }, [isLoading, isAuthenticated, getAccessTokenSilently]); + + // Rest of your app... +} +``` + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Invalid state" error | Clear browser storage and try again. Ensure `redirect_uri` matches configured callback URL | +| User stuck on loading | Check Auth0 application settings have correct callback URLs configured | +| API calls fail with 401 | Ensure `audience` is configured in Auth0Provider and matches your API identifier | +| Logout doesn't work | Include `returnTo` URL in logout options and configure in Auth0 "Allowed Logout URLs" | +| CORS errors when calling API | Add your application URL to "Allowed Web Origins" in Auth0 application settings | +| Tokens not refreshing | Enable `useRefreshTokens={true}` in Auth0Provider and ensure refresh token rotation is enabled in Auth0 | + +--- + +## MFA Handling + +The `@auth0/auth0-react` SDK provides a built-in MFA API for handling Multi-Factor Authentication entirely within your app — no redirects to Universal Login required. Access it via the `mfa` property from `useAuth0()`. + +> **Note:** MFA support via SDKs is currently in Early Access. For a simpler approach that uses Universal Login to handle MFA automatically (no custom UI), see the [Step-Up via Popup](#step-up-via-popup-simpler-approach) section below. + +### Catching MfaRequiredError + +When `getAccessTokenSilently()` encounters an MFA requirement, it throws `MfaRequiredError`. Catch it and inspect `mfa_requirements` to determine the flow: + +```tsx +import { useAuth0, MfaRequiredError } from '@auth0/auth0-react'; +import { useState } from 'react'; + +export function ProtectedApiCall() { + const { getAccessTokenSilently, mfa } = useAuth0(); + const [mfaToken, setMfaToken] = useState(null); + const [error, setError] = useState(null); + + const callApi = async () => { + try { + const token = await getAccessTokenSilently(); + // Use token to call API... + } catch (err) { + if (err instanceof MfaRequiredError) { + setMfaToken(err.mfa_token); + + // Check if enrollment or challenge is needed + const factors = await mfa.getEnrollmentFactors(err.mfa_token); + if (factors.length > 0) { + // User needs to enroll — show enrollment UI + } else { + // User has authenticators — show challenge UI + const authenticators = await mfa.getAuthenticators(err.mfa_token); + // Let user pick authenticator and proceed with challenge + } + } else { + setError(err.message); + } + } + }; + + return ; +} +``` + +### OTP Enrollment + +When the user needs to set up MFA for the first time: + +```tsx +import { useAuth0, MfaEnrollmentError } from '@auth0/auth0-react'; +import { useState } from 'react'; + +export function OtpEnrollment({ mfaToken }: { mfaToken: string }) { + const { mfa } = useAuth0(); + const [barcodeUri, setBarcodeUri] = useState(null); + const [recoveryCodes, setRecoveryCodes] = useState(null); + const [error, setError] = useState(null); + + const startEnrollment = async () => { + try { + const enrollment = await mfa.enroll({ mfaToken, factorType: 'otp' }); + setBarcodeUri(enrollment.barcodeUri); + setRecoveryCodes(enrollment.recoveryCodes); + } catch (err) { + if (err instanceof MfaEnrollmentError) { + setError(err.error_description); + } + } + }; + + return ( +
+ + {barcodeUri && ( +
+

Scan this QR code with your authenticator app:

+ {/* Render barcodeUri as QR code using a library like qrcode.react */} + {barcodeUri} +
+ )} + {recoveryCodes && ( +
+

Save these recovery codes:

+
    + {recoveryCodes.map((code, i) =>
  • {code}
  • )} +
+
+ )} +
+ ); +} +``` + +### Challenge and Verify + +When the user already has enrolled authenticators: + +```tsx +import { + useAuth0, + MfaChallengeError, + MfaVerifyError, +} from '@auth0/auth0-react'; +import { useState } from 'react'; + +export function MfaChallenge({ mfaToken }: { mfaToken: string }) { + const { mfa } = useAuth0(); + const [otp, setOtp] = useState(''); + const [error, setError] = useState(null); + + const handleVerify = async () => { + try { + // For OTP authenticators, you can skip challenge() and go straight to verify() + const tokens = await mfa.verify({ mfaToken, otp }); + // User is now authenticated — tokens are cached by the SDK + // Access token available at tokens.access_token + } catch (err) { + if (err instanceof MfaVerifyError) { + setError('Invalid code. Please try again.'); + } else if (err instanceof MfaChallengeError) { + setError('Challenge failed: ' + err.error_description); + } + } + }; + + return ( +
+

Enter your verification code

+ setOtp(e.target.value)} + placeholder="6-digit code" + maxLength={6} + /> + + {error &&

{error}

} +
+ ); +} +``` + +### SMS/Email Challenge (Out-of-Band) + +For SMS, Email, Voice, or Push authenticators, you must call `challenge()` first to send the code: + +```tsx +// Initiate challenge to send code via SMS/Email +const response = await mfa.challenge({ + mfaToken, + challengeType: 'oob', + authenticatorId: authenticator.id, +}); + +// Verify with the OOB code and the binding code the user received +const tokens = await mfa.verify({ + mfaToken, + oobCode: response.oobCode, + bindingCode: userEnteredCode, +}); +``` + +### Step-Up via Popup (Simpler Approach) + +If you don't need a custom MFA UI, configure `interactiveErrorHandler` to let the SDK handle MFA automatically via a Universal Login popup: + +```tsx + + + +``` + +With this setup, `getAccessTokenSilently()` automatically opens a popup when MFA is required. No error handling needed — the token is returned after the user completes MFA in the popup. + +--- + +## Security Considerations + +### Client-Side Security + +- **Never expose client secret** - React is client-side, use only public client credentials +- **Use PKCE** - Enabled by default with @auth0/auth0-react +- **Validate tokens on backend** - Never trust client-side token validation +- **Use HTTPS in production** - Auth0 requires HTTPS for production redirect URLs +- **Implement proper CORS** - Configure allowed origins in Auth0 application settings + +### Token Storage + +```tsx +// Default: memory storage for highest security (tokens cleared on page refresh) + + +// Or localstorage for better UX (tokens persist across refreshes) + +``` + +### Secure API Calls + +Always validate tokens on your backend: + +**Installation:** +```bash +npm install express-oauth2-jwt-bearer +``` + +**Backend validation example (Node.js):** +```javascript +const { auth, requiredScopes } = require('express-oauth2-jwt-bearer'); + +const checkJwt = auth({ + audience: process.env.AUTH0_AUDIENCE, + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, +}); + +app.get('/api/private', checkJwt, (req, res) => { + res.json({ message: 'Secured data' }); +}); + +// With scope validation +app.get('/api/users', checkJwt, requiredScopes('read:users'), (req, res) => { + res.json({ users: [] }); +}); +``` + +--- + +## Advanced Patterns + +### Custom Login with Redirect Options + +```tsx +const { loginWithRedirect } = useAuth0(); + +// Login with specific connection +await loginWithRedirect({ + authorizationParams: { + connection: 'google-oauth2' + } +}); + +// Login with prompt +await loginWithRedirect({ + authorizationParams: { + prompt: 'login' // Force login even if user has session + } +}); + +// Login with custom state +await loginWithRedirect({ + appState: { targetUrl: '/protected-page' } +}); +``` + +### Handle Redirect Callback + +```tsx +import { useAuth0 } from '@auth0/auth0-react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export function Callback() { + const { handleRedirectCallback } = useAuth0(); + const navigate = useNavigate(); + + useEffect(() => { + (async () => { + const result = await handleRedirectCallback(); + const targetUrl = result.appState?.targetUrl || '/'; + navigate(targetUrl); + })(); + }, [handleRedirectCallback, navigate]); + + return
Processing login...
; +} +``` + +### Custom Logout + +```tsx +const { logout } = useAuth0(); + +// Logout with custom return URL +logout({ + logoutParams: { + returnTo: `${window.location.origin}/goodbye` + } +}); + +// Logout without redirect (federated logout) +logout({ + logoutParams: { + federated: true + } +}); +``` + +--- + +## Testing + +### Manual Testing Checklist + +1. **Login Flow** + - Start dev server: `npm run dev` (Vite) or `npm start` (CRA) + - Click "Login" button + - Complete Auth0 Universal Login + - Verify redirect back to your app with user authenticated + - Check user profile displays correctly + +2. **Logout Flow** + - Click "Logout" button + - Verify user is logged out + - Verify redirect to correct page + +3. **Protected Routes** + - Navigate to protected route while logged out + - Verify redirect to Auth0 login + - After login, verify redirect back to protected route + +4. **API Calls** + - Call protected API endpoint + - Verify access token is included in request + - Verify API responds correctly + +--- + +## Next Steps + +- [API Reference](api.md) - Complete SDK documentation, configuration options, hooks reference +- [Setup Guide](setup.md) - Detailed setup instructions and scripts +- [Main Skill](../SKILL.md) - Return to main skill guide diff --git a/main/.mintlify/skills/auth0-react/references/setup.md b/main/.mintlify/skills/auth0-react/references/setup.md new file mode 100644 index 0000000000..0dae58058e --- /dev/null +++ b/main/.mintlify/skills/auth0-react/references/setup.md @@ -0,0 +1,363 @@ +# Auth0 React Setup Guide + +Complete setup instructions with automated scripts and manual configuration options. + +--- + +## Quick Setup (Automated) + +**Never read the contents of `.env` at any point during setup.** The file may contain sensitive secrets that should not be exposed in the LLM context. If you determine you need to read the file for any reason, ask the user for explicit permission before doing so — do not proceed until the user confirms. + +**Before running any part of this setup that writes to `.env`, you MUST ask the user for explicit confirmation.** Follow the steps below precisely. + +### Step 1: Check for existing .env and confirm with user + +Before writing to `.env`, check whether the file already exists: + +```bash +test -f .env && echo "EXISTS" || echo "NOT_FOUND" +``` + +Then ask the user for explicit confirmation before proceeding — do not continue until the user confirms: + +- If `.env` does **not** exist, ask: + - Question: "This setup will create a `.env` file containing Auth0 credentials (domain and client ID). Do you want to proceed?" + - Options: "Yes, create .env" / "No, I'll configure it manually" + +- If `.env` **already exists**, ask: + - Question: "A `.env` file already exists and may contain secrets unrelated to Auth0. This setup will append Auth0 credentials to it without modifying existing content. Do you want to proceed?" + - Options: "Yes, append to existing .env" / "No, I'll update it manually" + +**Do not proceed with writing to `.env` unless the user selects the confirmation option.** + +### Step 2: Run automated setup (only after confirmation) + +#### Bash Script (macOS/Linux) + +Run this script to automatically set up everything: + +```bash +#!/bin/bash + +# Detect OS and install Auth0 CLI if needed +if ! command -v auth0 &> /dev/null; then + echo "Installing Auth0 CLI..." + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install auth0/auth0-cli/auth0 + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Download and review the install script before executing + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh + echo "⚠️ Review the install script at /tmp/auth0-install.sh before running" + sh /tmp/auth0-install.sh -b /usr/local/bin + rm /tmp/auth0-install.sh + elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + echo "Please install Auth0 CLI: https://github.com/auth0/auth0-cli#installation" + exit 1 + fi +fi + +# Check if logged in to Auth0 +if ! auth0 tenants list &> /dev/null; then + echo "" + echo "======================================" + echo "Auth0 Login Required" + echo "======================================" + echo "" + read -p "Do you have an Auth0 account? (y/n): " HAS_ACCOUNT + + if [[ "$HAS_ACCOUNT" != "y" ]]; then + echo "" + echo "Let's create your free Auth0 account!" + echo "" + echo "1. Visit: https://auth0.com/signup" + echo "2. Sign up with your email or GitHub" + echo "3. Choose a tenant domain (e.g., 'mycompany')" + echo "4. Complete the onboarding" + echo "" + read -p "Press Enter when you've created your account..." + fi + + echo "" + echo "Logging in to Auth0..." + echo "A browser will open for authentication." + echo "" + auth0 login + + if ! auth0 tenants list &> /dev/null; then + echo "❌ Login failed. Please try again or visit https://auth0.com/docs" + exit 1 + fi + + echo "✅ Successfully logged in to Auth0!" +fi + +# Detect if Vite or CRA +if grep -q '"vite"' package.json 2>/dev/null; then + PREFIX="VITE_AUTH0" +elif grep -q '"react-scripts"' package.json 2>/dev/null; then + PREFIX="REACT_APP_AUTH0" +else + echo "Detecting React project type..." + PREFIX="VITE_AUTH0" # Default to Vite +fi + +# List apps and prompt for selection +echo "Your Auth0 applications:" +auth0 apps list + +read -p "Enter your Auth0 app ID (or press Enter to create a new one): " APP_ID + +if [ -z "$APP_ID" ]; then + echo "Creating new Auth0 SPA application..." + APP_NAME="${PWD##*/}-react-app" + APP_ID=$(auth0 apps create \ + --name "$APP_NAME" \ + --type spa \ + --auth-method None \ + --callbacks "http://localhost:3000,http://localhost:5173" \ + --logout-urls "http://localhost:3000,http://localhost:5173" \ + --origins "http://localhost:3000,http://localhost:5173" \ + --web-origins "http://localhost:3000,http://localhost:5173" \ + --metadata "created_by=agent_skills" \ + --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + echo "Created app with ID: $APP_ID" +fi + +# Get app details and create .env file +echo "Fetching Auth0 credentials..." +AUTH0_DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) +AUTH0_CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + +# Append Auth0 credentials to .env +cat >> .env << EOF +${PREFIX}_DOMAIN=$AUTH0_DOMAIN +${PREFIX}_CLIENT_ID=$AUTH0_CLIENT_ID +EOF + +echo "✅ Auth0 configuration complete!" +echo "Appended to .env:" +echo " ${PREFIX}_DOMAIN=$AUTH0_DOMAIN" +echo " ${PREFIX}_CLIENT_ID=$AUTH0_CLIENT_ID" +``` + +#### PowerShell Script (Windows) + +```powershell +# Install Auth0 CLI if not present +if (!(Get-Command auth0 -ErrorAction SilentlyContinue)) { + Write-Host "Installing Auth0 CLI..." + scoop install auth0 +} + +# Check if logged in +try { + auth0 tenants list | Out-Null +} catch { + Write-Host "" + Write-Host "======================================" + Write-Host "Auth0 Login Required" + Write-Host "======================================" + Write-Host "" + + $hasAccount = Read-Host "Do you have an Auth0 account? (y/n)" + + if ($hasAccount -ne "y") { + Write-Host "" + Write-Host "Let's create your free Auth0 account!" + Write-Host "" + Write-Host "1. Visit: https://auth0.com/signup" + Write-Host "2. Sign up with your email or GitHub" + Write-Host "3. Choose a tenant domain (e.g., 'mycompany')" + Write-Host "4. Complete the onboarding" + Write-Host "" + Read-Host "Press Enter when you've created your account" + } + + Write-Host "" + Write-Host "Logging in to Auth0..." + Write-Host "A browser will open for authentication." + Write-Host "" + auth0 login + + try { + auth0 tenants list | Out-Null + Write-Host "✅ Successfully logged in to Auth0!" + } catch { + Write-Host "❌ Login failed. Please try again or visit https://auth0.com/docs" + exit 1 + } +} + +# Detect project type +$prefix = if (Select-String -Path "package.json" -Pattern '"vite"' -Quiet) { "VITE_AUTH0" } + elseif (Select-String -Path "package.json" -Pattern '"react-scripts"' -Quiet) { "REACT_APP_AUTH0" } + else { "VITE_AUTH0" } + +# List and select app +Write-Host "Your Auth0 applications:" +auth0 apps list + +$appId = Read-Host "Enter your Auth0 app ID (or press Enter to create new)" + +if ([string]::IsNullOrEmpty($appId)) { + $appName = Split-Path -Leaf (Get-Location) + Write-Host "Creating new Auth0 SPA application..." + $appJson = auth0 apps create --name "$appName-react-app" --type spa ` + --auth-method None ` + --callbacks "http://localhost:3000,http://localhost:5173" ` + --logout-urls "http://localhost:3000,http://localhost:5173" ` + --origins "http://localhost:3000,http://localhost:5173" ` + --web-origins "http://localhost:3000,http://localhost:5173" ` + --metadata "created_by=agent_skills" --json + + $appId = ($appJson | ConvertFrom-Json).client_id + Write-Host "Created app with ID: $appId" +} + +# Get credentials and create .env +Write-Host "Fetching Auth0 credentials..." +$appDetails = auth0 apps show $appId --json | ConvertFrom-Json + +@" +${prefix}_DOMAIN=$($appDetails.domain) +${prefix}_CLIENT_ID=$($appDetails.client_id) +"@ | Out-File -FilePath .env -Encoding UTF8 -Append + +Write-Host "✅ Auth0 configuration complete!" +Write-Host "Appended to .env:" +Write-Host " ${prefix}_DOMAIN=$($appDetails.domain)" +Write-Host " ${prefix}_CLIENT_ID=$($appDetails.client_id)" +``` + +--- + +## Manual Setup + +If you prefer manual setup or the scripts don't work: + +### Step 1: Install SDK + +```bash +npm install @auth0/auth0-react +``` + +### Step 2: Install Auth0 CLI + +**macOS:** +```bash +brew install auth0/auth0-cli/auth0 +``` + +**Linux (review script before executing):** +```bash +curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh +# Review the script before running: cat /tmp/auth0-install.sh +sh /tmp/auth0-install.sh +rm /tmp/auth0-install.sh +``` + +**Windows:** +```powershell +scoop install auth0 +# Or: choco install auth0-cli +``` + +### Step 3: Get Credentials + +```bash +# Login to Auth0 +auth0 login + +# List your apps +auth0 apps list + +# Get app details (replace ) +auth0 apps show +``` + +### Step 4: Create .env File + +**For Vite:** +```bash +VITE_AUTH0_DOMAIN=.auth0.com +VITE_AUTH0_CLIENT_ID= +``` + +**For Create React App:** +```bash +REACT_APP_AUTH0_DOMAIN=.auth0.com +REACT_APP_AUTH0_CLIENT_ID= +``` + +--- + +## Creating an Auth0 Application via Dashboard + +If you prefer using the Auth0 Dashboard instead of the CLI: + +1. Go to [Auth0 Dashboard](https://manage.auth0.com) +2. Navigate to **Applications** → **Applications** +3. Click **Create Application** +4. Choose: + - Name: Your app name + - Type: **Single Page Web Applications** +5. Configure: + - **Allowed Callback URLs**: `http://localhost:3000, http://localhost:5173` + - **Allowed Logout URLs**: `http://localhost:3000, http://localhost:5173` + - **Allowed Web Origins**: `http://localhost:3000, http://localhost:5173` + - **Allowed Origins (CORS)**: `http://localhost:3000, http://localhost:5173` +6. Copy your **Domain** and **Client ID** +7. Create `.env` file as shown in Step 4 above + +--- + +## Troubleshooting Setup + +### CLI Installation Issues + +**macOS - Homebrew not found:** +```bash +# Install Homebrew first +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +**Windows - Scoop not found:** +```powershell +# Install Scoop first +iwr -useb get.scoop.sh | iex +``` + +### Login Issues + +**Browser doesn't open:** +```bash +# Use device code flow +auth0 login --no-browser +``` + +**"Not logged in" error:** +```bash +# Force new login +auth0 login --force +``` + +### Environment Variable Issues + +**Variables not loading (Vite):** +- Ensure variables start with `VITE_` +- Restart dev server after creating `.env` +- Check file is named exactly `.env` (not `.env.local`) + +**Variables not loading (CRA):** +- Ensure variables start with `REACT_APP_` +- Restart dev server after creating `.env` +- CRA doesn't support `.env` hot reload + +--- + +## Next Steps + +After setup is complete: +1. Return to [main skill guide](../SKILL.md) for integration steps +2. See [Integration Guide](integration.md) for advanced patterns +3. Check [API Reference](api.md) for complete SDK documentation diff --git a/main/.mintlify/skills/auth0-spa-js/SKILL.md b/main/.mintlify/skills/auth0-spa-js/SKILL.md index 846b24e928..e687ddd241 100644 --- a/main/.mintlify/skills/auth0-spa-js/SKILL.md +++ b/main/.mintlify/skills/auth0-spa-js/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding authentication to Vanilla JS, Svelte, or any framew license: Proprietary metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 SPA JS Integration @@ -167,6 +171,7 @@ const response = await fetch('https://your-api.example.com/data', { - [auth0-angular](/auth0-angular) — Auth0 for Angular SPAs - [auth0-vue](/auth0-vue) — Auth0 for Vue 3 SPAs - [auth0-mfa](/auth0-mfa) — Add Multi-Factor Authentication +- [auth0-cli](/auth0-cli) — Manage Auth0 resources from the terminal ## Quick Reference diff --git a/main/.mintlify/skills/auth0-spa-js/references/api.md b/main/.mintlify/skills/auth0-spa-js/references/api.md new file mode 100644 index 0000000000..4fcb629d5c --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/references/api.md @@ -0,0 +1,193 @@ +# Auth0 SPA JS — API Reference & Testing + +--- + +## Configuration Reference + +### Auth0ClientOptions + +Options passed to `createAuth0Client()` or `new Auth0Client()`. + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `domain` | `string` | Yes | Auth0 tenant domain — hostname only, no `https://` prefix | +| `clientId` | `string` | Yes | SPA application Client ID from Auth0 Dashboard | +| `authorizationParams` | `AuthorizationParams` | No | Authorization request parameters | +| `authorizationParams.redirect_uri` | `string` | No | Where Auth0 redirects after login (default: `window.location.origin`) | +| `authorizationParams.audience` | `string` | No | API identifier (e.g., `https://api.example.com`) for access tokens | +| `authorizationParams.scope` | `string` | No | Space-separated OIDC scopes (default: `openid profile email`) | +| `authorizationParams.organization` | `string` | No | Organization ID or name for multi-tenant apps | +| `useRefreshTokens` | `boolean` | No | Enable refresh token rotation (default: `false`) | +| `useRefreshTokensFallback` | `boolean` | No | Fall back to iframe silent auth if refresh token fails (default: `false`) | +| `cacheLocation` | `'memory' \| 'localstorage'` | No | Token cache location (default: `'memory'`) | +| `cache` | `ICache` | No | Custom cache implementation | +| `useDpop` | `boolean` | No | Enable DPoP token binding (default: `false`) | +| `useMrrt` | `boolean` | No | Multi-resource refresh tokens — requires `useRefreshTokens: true` (default: `false`) | +| `leeway` | `number` | No | Clock skew tolerance in seconds (default: `60`) | +| `sessionCheckExpiryDays` | `number` | No | Days before session check cookie expires (default: `1`) | +| `httpTimeoutInSeconds` | `number` | No | HTTP request timeout (default: `10`) | +| `issuer` | `string` | No | Override expected token issuer | + +### getTokenSilently Options + +| Option | Type | Description | +|--------|------|-------------| +| `authorizationParams.audience` | `string` | Override audience for this token request | +| `authorizationParams.scope` | `string` | Override scopes for this token request | +| `cacheMode` | `'on' \| 'off' \| 'cache-only'` | Cache behavior (default: `'on'`) | +| `detailedResponse` | `boolean` | Return `{ access_token, token_type, id_token, expires_in }` instead of string | +| `timeoutInSeconds` | `number` | Override timeout for this call | + +--- + +## Environment Variables + +| Bundler | Domain Variable | Client ID Variable | +|---------|----------------|-------------------| +| Vite | `VITE_AUTH0_DOMAIN` | `VITE_AUTH0_CLIENT_ID` | +| Create React App | `REACT_APP_AUTH0_DOMAIN` | `REACT_APP_AUTH0_CLIENT_ID` | +| Webpack (custom) | `AUTH0_DOMAIN` | `AUTH0_CLIENT_ID` | + +**Vite access:** +```js +import.meta.env.VITE_AUTH0_DOMAIN +``` + +**CRA access:** +```js +process.env.REACT_APP_AUTH0_DOMAIN +``` + +--- + +## Error Types + +| Class | Import | When Thrown | +|-------|--------|-------------| +| `AuthenticationError` | `@auth0/auth0-spa-js` | `handleRedirectCallback` — Auth0 returned an error | +| `GenericError` | `@auth0/auth0-spa-js` | Network or Auth0 API errors; base class for all SDK errors | +| `TimeoutError` | `@auth0/auth0-spa-js` | Silent auth or network request timeout | +| `PopupTimeoutError` | `@auth0/auth0-spa-js` | `loginWithPopup` — user didn't complete in time | +| `PopupCancelledError` | `@auth0/auth0-spa-js` | `loginWithPopup` — popup was closed by the user | +| `PopupOpenError` | `@auth0/auth0-spa-js` | `loginWithPopup` — `window.open` returned null (popups blocked) | +| `MfaRequiredError` | `@auth0/auth0-spa-js` | `getTokenSilently` — MFA step required; access `error.mfa_token` | +| `MissingRefreshTokenError` | `@auth0/auth0-spa-js` | `getTokenSilently` — refresh token not available | +| `ConnectError` | `@auth0/auth0-spa-js` | `handleRedirectCallback` — error in connected accounts flow | +| `MfaListAuthenticatorsError` | `@auth0/auth0-spa-js` | `auth0.mfa.getAuthenticators()` failed | +| `MfaEnrollmentError` | `@auth0/auth0-spa-js` | `auth0.mfa.enroll()` failed | +| `MfaChallengeError` | `@auth0/auth0-spa-js` | `auth0.mfa.challenge()` failed | +| `MfaVerifyError` | `@auth0/auth0-spa-js` | `auth0.mfa.verify()` failed | + +--- + +## Claims Reference + +Claims available from `auth0.getUser()` (ID token): + +| Claim | Type | Description | +|-------|------|-------------| +| `sub` | `string` | Subject — unique user ID: `auth0\|64abc...` | +| `name` | `string` | Full name | +| `given_name` | `string` | First name | +| `family_name` | `string` | Last name | +| `nickname` | `string` | Nickname or username | +| `email` | `string` | Email address | +| `email_verified` | `boolean` | Whether email is verified | +| `picture` | `string` | Profile picture URL | +| `locale` | `string` | User locale | +| `updated_at` | `string` | Last profile update ISO timestamp | + +Claims on the **access token** (from `getTokenSilently({ detailedResponse: true })`): + +| Claim | Description | +|-------|-------------| +| `iss` | Issuer — your Auth0 domain URL | +| `aud` | Audience — API identifier(s) | +| `azp` | Authorized party — your Client ID | +| `scope` | Space-separated scopes granted | +| `permissions` | RBAC permissions array (requires API audience + Auth0 RBAC enabled) | +| `org_id` | Organization ID for multi-tenant apps | + +--- + +## Testing Checklist + +### Core Authentication + +- [ ] Login redirect sends user to Auth0 Universal Login page +- [ ] After login, user is returned to app (no dangling `code=` params in URL) +- [ ] `auth0.isAuthenticated()` returns `true` after successful login +- [ ] `auth0.getUser()` returns profile with `sub`, `name`, `email` +- [ ] Logout clears session and redirects to `returnTo` URL +- [ ] After logout, `isAuthenticated()` returns `false` + +### Token Management + +- [ ] `getTokenSilently()` returns a JWT string +- [ ] Access token decoded at [jwt.io](https://jwt.io) shows correct `aud`, `iss`, `sub` +- [ ] Tokens are **not** stored in `localStorage` (DevTools → Application → Local Storage) +- [ ] Page refresh maintains authentication (silent auth via `checkSession`) +- [ ] `getTokenSilently()` works without redirecting when session is active + +### Error Handling + +- [ ] Navigating to app when not logged in does not throw uncaught errors +- [ ] `login_required` error on `getTokenSilently` triggers re-authentication +- [ ] Network failure in `getTokenSilently` is caught and handled gracefully + +### Security + +- [ ] Auth0 Dashboard: Application type is **Single Page Application** +- [ ] Auth0 Dashboard: Token Endpoint Auth Method is **None** +- [ ] Auth0 Dashboard: Allowed Web Origins includes your app origin +- [ ] No `client_secret` anywhere in source code or `.env` +- [ ] Dev `.env` file is in `.gitignore` + +--- + +## Common Issues + +| Error | Cause | Fix | +|-------|-------|-----| +| `login_required` on `getTokenSilently` | Allowed Web Origins not configured | Add `http://localhost:5173` to Allowed Web Origins in Auth0 Dashboard | +| `invalid_client` | Wrong Client ID | Check env var matches Auth0 Dashboard Client ID | +| Callback URL mismatch error | Port mismatch between app and Dashboard | Match exactly: `http://localhost:5173` in both places | +| `Unable to open popup` | Popup not triggered by user gesture | Call `loginWithPopup` directly in a click handler, never from async init | +| Token not refreshing silently | `offline_access` scope missing | Add `scope: 'openid profile email offline_access'` with `useRefreshTokens: true` | +| `MissingRefreshTokenError` | `useRefreshTokens` false or scope missing | Enable `useRefreshTokens: true` and include `offline_access` scope | +| User logged out on page refresh | Allowed Web Origins missing or no refresh tokens | Add Allowed Web Origins; enable `useRefreshTokens: true` | +| Cross-origin iframe blocked | Browser blocks third-party cookies | Use `useRefreshTokens: true` instead of silent iframe auth | +| Domain includes protocol | `domain` option should not include `https://` | Use `your-tenant.auth0.com` not `https://your-tenant.auth0.com` | + +--- + +## Security Considerations + +### Token Storage + +| Strategy | Security | Session Persistence | +|----------|----------|-------------------| +| In-memory (default) | Highest — immune to XSS | Lost on page refresh | +| Refresh tokens (`useRefreshTokens: true`) | High — refresh token in memory | Persists across page refreshes | +| `localStorage` | Lowest — vulnerable to XSS | Persists across page refreshes | + +**Recommendation:** Use `useRefreshTokens: true` with `cacheLocation: 'memory'` (the default) for the best balance of security and user experience. + +### No Client Secret + +SPAs run entirely in the browser and cannot protect secrets. The Auth0 SPA application type explicitly disables client secret authentication. Never add `client_secret` to a browser-based application. + +### PKCE Flow + +`@auth0/auth0-spa-js` always uses the Authorization Code + PKCE (Proof Key for Code Exchange) flow. This protects against authorization code interception and is the only secure OAuth 2.0 flow for browser-based applications. + +### Content Security Policy + +If you need to restrict iframe origins (only relevant when NOT using `useRefreshTokens`): +``` +Content-Security-Policy: frame-src https://your-tenant.auth0.com; +``` + +### XSS Protection + +Never use `cacheLocation: 'localstorage'` in production unless you have fully mitigated all XSS risks. XSS can steal tokens from `localStorage`. The default in-memory cache is immune to XSS-based token theft. diff --git a/main/.mintlify/skills/auth0-spa-js/references/integration.md b/main/.mintlify/skills/auth0-spa-js/references/integration.md new file mode 100644 index 0000000000..de9e2e8542 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/references/integration.md @@ -0,0 +1,464 @@ +# Auth0 SPA JS Integration Patterns + +--- + +## Client Initialization + +### Using createAuth0Client (Recommended) + +`createAuth0Client` initializes the client and automatically calls `checkSession()` to restore any existing session: + +```js +import { createAuth0Client } from '@auth0/auth0-spa-js'; + +const auth0 = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + authorizationParams: { + redirect_uri: window.location.origin + } +}); +``` + +### Using Auth0Client Directly + +Use when you need more control over initialization order: + +```js +import { Auth0Client } from '@auth0/auth0-spa-js'; + +const auth0 = new Auth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + authorizationParams: { + redirect_uri: window.location.origin + } +}); + +// Manually check existing session +try { + await auth0.getTokenSilently(); +} catch (error) { + if (error.error !== 'login_required') { + throw error; + } +} +``` + +--- + +## Login + +### Login with Redirect + +```js +// Basic redirect login +await auth0.loginWithRedirect(); + +// With additional parameters +await auth0.loginWithRedirect({ + authorizationParams: { + audience: 'https://api.example.com', + scope: 'openid profile email read:data' + } +}); +``` + +### Handle Redirect Callback + +Call this on page load to process the redirect result after Auth0 returns the user: + +```js +const query = new URLSearchParams(window.location.search); +if ((query.has('code') || query.has('error')) && query.has('state')) { + try { + const result = await auth0.handleRedirectCallback(); + // result.appState contains data you passed via loginWithRedirect + console.log('App state:', result.appState); + } catch (err) { + console.error('Redirect callback failed:', err); + } + // Clean up URL after processing + window.history.replaceState({}, document.title, window.location.pathname); +} +``` + +### Login with Popup + +Use when you want to avoid a full-page redirect (must be triggered directly by a user click): + +```js +document.getElementById('login-popup-btn').addEventListener('click', async () => { + try { + await auth0.loginWithPopup(); + const user = await auth0.getUser(); + console.log('Logged in:', user.name); + } catch (err) { + if (err.error !== 'popup_cancelled') { + console.error('Popup login failed:', err); + } + } +}); +``` + +--- + +## Logout + +```js +// Logout and return to app origin +auth0.logout({ + logoutParams: { + returnTo: window.location.origin + } +}); + +// Logout without redirect (clear local session only) +auth0.logout({ openUrl: false }); + +// Logout and redirect to custom URL +auth0.logout({ + logoutParams: { + returnTo: 'https://your-app.example.com/logged-out' + } +}); +``` + +--- + +## User Profile + +```js +// Check authentication state +const isAuthenticated = await auth0.isAuthenticated(); + +// Get user profile (returns undefined if not authenticated) +const user = await auth0.getUser(); +if (user) { + console.log(user.sub); // Auth0 user ID + console.log(user.name); // Full name + console.log(user.email); // Email address + console.log(user.picture); // Profile picture URL + console.log(user.email_verified); // Boolean +} +``` + +--- + +## Protecting Content + +Show/hide content based on authentication state: + +```js +async function updateUI() { + const isAuthenticated = await auth0.isAuthenticated(); + + // Toggle login/logout buttons + document.getElementById('btn-login').style.display = isAuthenticated ? 'none' : 'block'; + document.getElementById('btn-logout').style.display = isAuthenticated ? 'block' : 'none'; + + // Show user profile section + const profileSection = document.getElementById('profile'); + if (profileSection) { + profileSection.style.display = isAuthenticated ? 'block' : 'none'; + } + + if (isAuthenticated) { + const user = await auth0.getUser(); + document.getElementById('user-name').textContent = user.name; + document.getElementById('user-email').textContent = user.email; + if (document.getElementById('user-picture')) { + document.getElementById('user-picture').src = user.picture; + } + } +} + +// Call on page load and after auth state changes +await updateUI(); +``` + +--- + +## Calling Protected APIs + +```js +// Get access token silently (uses cache first, refreshes if expired) +async function callApi(url) { + const accessToken = await auth0.getTokenSilently(); + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} + +// Usage +document.getElementById('call-api-btn').addEventListener('click', async () => { + try { + const data = await callApi('https://your-api.example.com/private'); + document.getElementById('result').textContent = JSON.stringify(data, null, 2); + } catch (err) { + console.error('API call failed:', err); + } +}); +``` + +### Get Detailed Token Response + +```js +const { access_token, token_type, id_token, expires_in } = await auth0.getTokenSilently({ + detailedResponse: true +}); +``` + +### Token for a Specific Audience + +```js +const token = await auth0.getTokenSilently({ + authorizationParams: { + audience: 'https://api.example.com', + scope: 'read:data write:data' + } +}); +``` + +--- + +## Refresh Token Rotation + +Enable to maintain sessions across page refreshes without relying on third-party cookies (recommended for modern browsers): + +```js +const auth0 = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + useRefreshTokens: true, + authorizationParams: { + redirect_uri: window.location.origin, + scope: 'openid profile email offline_access' // offline_access required + } +}); +``` + +> **Note:** Enable **Allow Offline Access** on your Auth0 API in the Dashboard for `offline_access` scope to work. + +--- + +## Organizations + +### Login to a Specific Organization + +```js +await auth0.loginWithRedirect({ + authorizationParams: { + organization: 'org_xxxxxxxxxxxx' // or organization name + } +}); +``` + +### Initialize Client with Organization + +```js +const auth0 = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + authorizationParams: { + redirect_uri: window.location.origin, + organization: 'org_xxxxxxxxxxxx' + } +}); +``` + +### Switch Organizations + +```js +async function switchOrganization(orgId) { + await auth0.logout({ openUrl: false }); + await auth0.loginWithRedirect({ + authorizationParams: { organization: orgId } + }); +} +``` + +### Accept User Invitations + +```js +const url = new URL(window.location.href); +const organization = url.searchParams.get('organization'); +const invitation = url.searchParams.get('invitation'); + +if (organization && invitation) { + await auth0.loginWithRedirect({ + authorizationParams: { organization, invitation } + }); +} +``` + +--- + +## MFA Handling + +Handle MFA when `getTokenSilently()` requires a second factor: + +```js +import { MfaRequiredError } from '@auth0/auth0-spa-js'; + +try { + const token = await auth0.getTokenSilently(); +} catch (error) { + if (error instanceof MfaRequiredError) { + // Trigger MFA challenge via popup or redirect + await auth0.loginWithPopup({ + authorizationParams: { + mfa_token: error.mfa_token + } + }); + } +} +``` + +--- + +## DPoP (Device-Bound Tokens) + +Enable DPoP to bind access tokens to the client's cryptographic key pair: + +```js +const auth0 = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + useDpop: true, + authorizationParams: { + redirect_uri: window.location.origin + } +}); + +// Use createFetcher to automatically handle DPoP proof generation +const fetcher = auth0.createFetcher({ dpopNonceId: 'my_api' }); + +const response = await fetcher.fetchWithAuth('https://api.example.com/data', { + method: 'GET' +}); +``` + +--- + +## Error Handling + +```js +import { + AuthenticationError, + GenericError, + TimeoutError, + PopupTimeoutError, + PopupCancelledError, + PopupOpenError, + MfaRequiredError, + MissingRefreshTokenError +} from '@auth0/auth0-spa-js'; + +// Handle redirect callback errors +try { + await auth0.handleRedirectCallback(); +} catch (err) { + if (err instanceof AuthenticationError) { + // Auth0 returned an error in the callback (e.g., access_denied) + console.error('Auth error:', err.error, err.error_description); + } else { + console.error('Unexpected error:', err); + } +} + +// Handle token errors +try { + const token = await auth0.getTokenSilently(); +} catch (err) { + if (err.error === 'login_required') { + // User needs to log in — redirect to login + await auth0.loginWithRedirect(); + } else if (err instanceof MissingRefreshTokenError) { + // Refresh token missing — user needs to re-authenticate + await auth0.loginWithRedirect(); + } else if (err instanceof TimeoutError) { + console.error('Request timed out'); + } else { + console.error('Token error:', err); + } +} + +// Handle popup errors +try { + await auth0.loginWithPopup(); +} catch (err) { + if (err instanceof PopupOpenError) { + console.error('Popups are blocked. Please allow popups for this site.'); + } else if (err instanceof PopupCancelledError) { + console.log('User closed the popup'); + } else if (err instanceof PopupTimeoutError) { + console.error('Popup timed out'); + } +} +``` + +--- + +## Authentication Flow + +``` +User clicks Login + ↓ +auth0.loginWithRedirect() + ↓ +Browser redirects to Auth0 Universal Login + ↓ +User enters credentials / social login + ↓ +Auth0 redirects back to redirect_uri?code=xxx&state=xxx + ↓ +auth0.handleRedirectCallback() — exchanges code for tokens + ↓ +Tokens stored in memory (or refresh token if useRefreshTokens: true) + ↓ +auth0.isAuthenticated() → true +auth0.getUser() → user profile +auth0.getTokenSilently() → access token +``` + +--- + +## Testing Patterns + +### Test Authentication State + +```js +describe('Auth0 integration', () => { + it('should show login button when not authenticated', async () => { + const isAuthenticated = await auth0.isAuthenticated(); + expect(isAuthenticated).toBe(false); + expect(document.getElementById('btn-login').style.display).toBe('block'); + }); +}); +``` + +### Mock Auth0 Client in Tests + +```js +// Vitest / Jest +vi.mock('@auth0/auth0-spa-js', () => ({ + createAuth0Client: vi.fn().mockResolvedValue({ + isAuthenticated: vi.fn().mockResolvedValue(true), + getUser: vi.fn().mockResolvedValue({ name: 'Test User', email: 'test@example.com' }), + loginWithRedirect: vi.fn(), + logout: vi.fn(), + getTokenSilently: vi.fn().mockResolvedValue('mock-access-token'), + handleRedirectCallback: vi.fn().mockResolvedValue({ appState: null }) + }) +})); +``` diff --git a/main/.mintlify/skills/auth0-spa-js/references/setup.md b/main/.mintlify/skills/auth0-spa-js/references/setup.md new file mode 100644 index 0000000000..48c5a40dc7 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/references/setup.md @@ -0,0 +1,316 @@ +# Auth0 SPA JS Setup Guide + +Complete setup instructions with automated scripts and manual configuration options. + +--- + +## Quick Setup (Automated) + +**Never read the contents of `.env` at any point during setup.** The file may contain sensitive secrets that should not be exposed in the LLM context. If you determine you need to read the file for any reason, ask the user for explicit permission before doing so — do not proceed until the user confirms. + +**Before running any part of this setup that writes to `.env`, you MUST ask the user for explicit confirmation.** Follow the steps below precisely. + +### Step 1: Check for existing .env and confirm with user + +Before writing to `.env`, check whether the file already exists: + +```bash +test -f .env && echo "EXISTS" || echo "NOT_FOUND" +``` + +Then ask the user for explicit confirmation before proceeding — do not continue until the user confirms: + +- If `.env` does **not** exist, ask: + - Question: "This setup will create a `.env` file containing Auth0 credentials (domain and client ID). Do you want to proceed?" + - Options: "Yes, create .env" / "No, I'll configure it manually" + +- If `.env` **already exists**, ask: + - Question: "A `.env` file already exists and may contain secrets unrelated to Auth0. This setup will append Auth0 credentials to it without modifying existing content. Do you want to proceed?" + - Options: "Yes, append to existing .env" / "No, I'll update it manually" + +**Do not proceed with writing to `.env` unless the user selects the confirmation option.** + +### Step 2: Run automated setup (only after confirmation) + +#### Bash Script (macOS/Linux) + +```bash +#!/bin/bash + +# Install Auth0 CLI if needed +if ! command -v auth0 &> /dev/null; then + echo "Installing Auth0 CLI..." + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install auth0/auth0-cli/auth0 + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh -s -- -b /usr/local/bin + else + echo "Please install Auth0 CLI: https://github.com/auth0/auth0-cli#installation" + exit 1 + fi +fi + +# Check if logged in to Auth0 +if ! auth0 tenants list &> /dev/null; then + echo "" + echo "======================================" + echo "Auth0 Login Required" + echo "======================================" + read -p "Do you have an Auth0 account? (y/n): " HAS_ACCOUNT + + if [[ "$HAS_ACCOUNT" != "y" ]]; then + echo "" + echo "Create a free account at: https://auth0.com/signup" + read -p "Press Enter when you've created your account..." + fi + + auth0 login + if ! auth0 tenants list &> /dev/null; then + echo "❌ Login failed. Please try again." + exit 1 + fi + echo "✅ Successfully logged in to Auth0!" +fi + +# Detect env var prefix from project +if grep -q '"vite"' package.json 2>/dev/null; then + PREFIX="VITE_AUTH0" +elif grep -q '"react-scripts"' package.json 2>/dev/null; then + PREFIX="REACT_APP_AUTH0" +else + PREFIX="VITE_AUTH0" # Default to Vite +fi + +# List apps and prompt for selection +echo "Your Auth0 applications:" +auth0 apps list + +read -p "Enter your Auth0 app ID (or press Enter to create a new one): " APP_ID + +if [ -z "$APP_ID" ]; then + echo "Creating new Auth0 SPA application..." + APP_NAME="${PWD##*/}-spa" + APP_ID=$(auth0 apps create \ + --name "$APP_NAME" \ + --type spa \ + --auth-method None \ + --callbacks "http://localhost:3000,http://localhost:5173" \ + --logout-urls "http://localhost:3000,http://localhost:5173" \ + --origins "http://localhost:3000,http://localhost:5173" \ + --web-origins "http://localhost:3000,http://localhost:5173" \ + --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + echo "Created SPA app with ID: $APP_ID" +fi + +# Get app details +AUTH0_DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) +AUTH0_CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + +# Append to .env +cat >> .env << EOF +${PREFIX}_DOMAIN=$AUTH0_DOMAIN +${PREFIX}_CLIENT_ID=$AUTH0_CLIENT_ID +EOF + +echo "✅ Auth0 configuration complete!" +echo "Appended to .env:" +echo " ${PREFIX}_DOMAIN=$AUTH0_DOMAIN" +echo " ${PREFIX}_CLIENT_ID=$AUTH0_CLIENT_ID" +``` + +#### PowerShell Script (Windows) + +```powershell +# Install Auth0 CLI if not present +if (!(Get-Command auth0 -ErrorAction SilentlyContinue)) { + Write-Host "Installing Auth0 CLI..." + scoop install auth0 +} + +# Check if logged in +try { + auth0 tenants list | Out-Null +} catch { + Write-Host "Auth0 Login Required" + $hasAccount = Read-Host "Do you have an Auth0 account? (y/n)" + + if ($hasAccount -ne "y") { + Write-Host "Create a free account at: https://auth0.com/signup" + Read-Host "Press Enter when you've created your account" + } + + auth0 login + Write-Host "✅ Successfully logged in to Auth0!" +} + +# Detect env var prefix +$prefix = if (Select-String -Path "package.json" -Pattern '"vite"' -Quiet) { "VITE_AUTH0" } + elseif (Select-String -Path "package.json" -Pattern '"react-scripts"' -Quiet) { "REACT_APP_AUTH0" } + else { "VITE_AUTH0" } + +# List and select app +Write-Host "Your Auth0 applications:" +auth0 apps list + +$appId = Read-Host "Enter your Auth0 app ID (or press Enter to create new)" + +if ([string]::IsNullOrEmpty($appId)) { + $appName = Split-Path -Leaf (Get-Location) + Write-Host "Creating new Auth0 SPA application..." + $appJson = auth0 apps create --name "$appName-spa" --type spa ` + --auth-method None ` + --callbacks "http://localhost:3000,http://localhost:5173" ` + --logout-urls "http://localhost:3000,http://localhost:5173" ` + --origins "http://localhost:3000,http://localhost:5173" ` + --web-origins "http://localhost:3000,http://localhost:5173" ` + --json + + $appId = ($appJson | ConvertFrom-Json).client_id + Write-Host "Created app with ID: $appId" +} + +# Get credentials +$appDetails = auth0 apps show $appId --json | ConvertFrom-Json + +@" +${prefix}_DOMAIN=$($appDetails.domain) +${prefix}_CLIENT_ID=$($appDetails.client_id) +"@ | Out-File -FilePath .env -Encoding UTF8 -Append + +Write-Host "✅ Auth0 configuration complete!" +Write-Host " ${prefix}_DOMAIN=$($appDetails.domain)" +Write-Host " ${prefix}_CLIENT_ID=$($appDetails.client_id)" +``` + +--- + +## Manual Setup + +If you prefer manual setup or the scripts don't work: + +### Step 1: Install SDK + +```bash +npm install @auth0/auth0-spa-js +``` + +### Step 2: Install Auth0 CLI + +**macOS:** +```bash +brew install auth0/auth0-cli/auth0 +``` + +**Linux:** +```bash +curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh +``` + +**Windows:** +```powershell +scoop install auth0 +``` + +### Step 3: Get Credentials + +```bash +# Login to Auth0 +auth0 login + +# List your apps +auth0 apps list + +# Get app details (replace ) +auth0 apps show +``` + +### Step 4: Create .env File + +**For Vite-based projects:** +```bash +VITE_AUTH0_DOMAIN=your-tenant.auth0.com +VITE_AUTH0_CLIENT_ID=your-client-id +``` + +**For Create React App:** +```bash +REACT_APP_AUTH0_DOMAIN=your-tenant.auth0.com +REACT_APP_AUTH0_CLIENT_ID=your-client-id +``` + +**For Webpack / plain HTML:** +```bash +AUTH0_DOMAIN=your-tenant.auth0.com +AUTH0_CLIENT_ID=your-client-id +``` + +--- + +## Creating an Auth0 Application via Dashboard + +1. Go to [Auth0 Dashboard](https://manage.auth0.com) +2. Navigate to **Applications** → **Applications** +3. Click **Create Application** +4. Choose: + - Name: Your app name + - Type: **Single Page Web Applications** +5. Configure in the **Settings** tab: + - **Allowed Callback URLs**: `http://localhost:5173, http://localhost:3000` + - **Allowed Logout URLs**: `http://localhost:5173, http://localhost:3000` + - **Allowed Web Origins**: `http://localhost:5173, http://localhost:3000` + - **Allowed Origins (CORS)**: `http://localhost:5173, http://localhost:3000` +6. Click **Save Changes** +7. Copy your **Domain** and **Client ID** + +> **Important:** The **Allowed Web Origins** field is required for `getTokenSilently()` (silent authentication). Without it, users will be logged out on every page refresh. + +--- + +## Secret Management + +SPAs do **not** use a `client_secret`. The Auth0 SPA application type explicitly sets the Token Endpoint Authentication Method to `None`. If you see a client secret anywhere in your code, remove it immediately. + +Your `.env` file contains only: +- `AUTH0_DOMAIN` / `VITE_AUTH0_DOMAIN` — Not a secret (public) +- `AUTH0_CLIENT_ID` / `VITE_AUTH0_CLIENT_ID` — Not a secret (public) + +Still, follow these practices: +- Add `.env` to `.gitignore` (to avoid accidental commits with other sensitive env vars) +- Use `.env.local` for Vite projects (auto-ignored by Vite's default `.gitignore`) +- Never commit credential files to version control + +--- + +## Troubleshooting Setup + +### Environment Variables Not Loading + +**Vite:** +- Ensure variables start with `VITE_` +- Restart the dev server after creating/editing `.env` +- Use `import.meta.env.VITE_AUTH0_DOMAIN` (not `process.env`) + +**Create React App:** +- Ensure variables start with `REACT_APP_` +- Restart dev server after changes + +### Auth0 CLI Issues + +**Browser doesn't open for login:** +```bash +auth0 login --no-browser +``` + +**"Not logged in" error:** +```bash +auth0 login --force +``` + +--- + +## Next Steps + +After setup is complete: +1. Return to [main skill guide](../SKILL.md) for integration steps +2. See [Integration Guide](integration.md) for advanced patterns +3. Check [API Reference](api.md) for complete SDK documentation diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/bootstrap.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/bootstrap.mjs new file mode 100644 index 0000000000..262042ceb6 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/bootstrap.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +import path from "node:path" + +import { + checkNodeVersion, + checkAuth0CLI, + getActiveTenant, + validateSpaProject, +} from "./utils/validation.mjs" +import { + discoverExistingConnections, + buildChangePlan, + displayChangePlan, +} from "./utils/discovery.mjs" +import { applySpaClientChanges } from "./utils/clients.mjs" +import { applyDatabaseConnectionChanges, checkDatabaseConnectionChanges } from "./utils/connections.mjs" +import { writeEnvFile } from "./utils/env-writer.mjs" +import { confirmWithUser } from "./utils/helpers.mjs" + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + console.log("\n🚀 Auth0 SPA JS Bootstrap\n") + + // 1. Parse args — optional project path (defaults to cwd) + const projectPath = path.resolve(process.argv[2] || process.cwd()) + + // 2. Pre-flight checks + checkNodeVersion() + await checkAuth0CLI() + + // 3. Auto-detect tenant + const domain = await getActiveTenant() + + // 4. Validate SPA project + const spaConfig = validateSpaProject(projectPath) + + // 5. Discover existing connections + const connections = await discoverExistingConnections() + + // 6. Build change plan + const plan = buildChangePlan(connections, domain, spaConfig) + + // 7. Display plan + displayChangePlan(plan) + + // 8. Confirm with user + const confirmed = await confirmWithUser("Apply these changes?") + if (!confirmed) { + console.log("\n❌ Aborted by user.\n") + process.exit(0) + } + + // 9. Create SPA app + console.log("") + const client = await applySpaClientChanges(plan.client) + + // 10. Set up database connection with the real client_id + plan.connection = checkDatabaseConnectionChanges(connections, client.client_id) + await applyDatabaseConnectionChanges(plan.connection, client.client_id) + + // 11. Write .env file + const envFilePath = path.join(projectPath, ".env") + const envVars = getEnvVars(spaConfig.framework, domain, client.client_id) + await writeEnvFile(envVars, envFilePath) + + // 12. Summary + console.log("\n✅ Auth0 SPA JS Setup Complete\n") + console.log(` Domain: ${domain}`) + console.log(` Client ID: ${client.client_id}`) + console.log(` Framework: ${spaConfig.framework}`) + console.log(` Port: ${spaConfig.port}`) + console.log(` Callback: http://localhost:${spaConfig.port}`) + console.log("") + console.log(" Remaining manual steps:") + console.log(" 1. In Auth0 Dashboard → Application → Settings, verify:") + console.log(` Allowed Callback URLs: http://localhost:${spaConfig.port}`) + console.log(` Allowed Logout URLs: http://localhost:${spaConfig.port}`) + console.log(` Allowed Web Origins: http://localhost:${spaConfig.port}`) + console.log(" 2. Restart your dev server to pick up the new .env values") + console.log(" 3. Initialize createAuth0Client() with your env vars") + console.log("") +} + +/** + * Determine the correct env var prefix based on detected framework. + */ +function getEnvVars(framework, domain, clientId) { + const prefix = framework === "react-cra" ? "REACT_APP_AUTH0" : "VITE_AUTH0" + return { + [`${prefix}_DOMAIN`]: domain, + [`${prefix}_CLIENT_ID`]: clientId, + } +} + +main().catch((e) => { + console.error(`\n❌ Bootstrap failed: ${e.message}\n`) + process.exit(1) +}) diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/package.json b/main/.mintlify/skills/auth0-spa-js/scripts/package.json new file mode 100644 index 0000000000..96aaa38ae7 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "auth0-spa-js-bootstrap", + "version": "1.0.0", + "description": "Bootstrap Auth0 configuration for SPA JS projects", + "type": "module", + "scripts": { + "auth0:bootstrap": "node bootstrap.mjs" + }, + "dependencies": { + "@inquirer/prompts": "^8.1.0", + "execa": "^9.0.0", + "ora": "^8.0.0" + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/auth0-api.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/auth0-api.mjs new file mode 100644 index 0000000000..8fa0c75ed6 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/auth0-api.mjs @@ -0,0 +1,24 @@ +import { $ } from "execa" + +/** + * Make a generic API call using auth0 CLI. + */ +export async function auth0ApiCall(method, endpoint, data = null) { + const args = ["api", method, endpoint, "--no-input"] + + if (data) { + args.push("--data", JSON.stringify(data)) + } + + try { + const { stdout } = await $({ timeout: 30000 })`auth0 ${args}` + return stdout ? JSON.parse(stdout) : null + } catch (e) { + if (e.timedOut) { + console.warn(`⚠️ Warning: API call timed out: auth0 api ${method} ${endpoint}`) + } else { + console.warn(`⚠️ Warning: API call failed: ${e.message}`) + } + throw e + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/change-plan.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/change-plan.mjs new file mode 100644 index 0000000000..31a747c108 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/change-plan.mjs @@ -0,0 +1,12 @@ +export const ChangeAction = { + CREATE: "create", + UPDATE: "update", + SKIP: "skip", +} + +export function createChangeItem(action, details = {}) { + return { + action, + ...details, + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/clients.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/clients.mjs new file mode 100644 index 0000000000..fa403a2135 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/clients.mjs @@ -0,0 +1,40 @@ +import { $ } from "execa" +import ora from "ora" + +import { ChangeAction, createChangeItem } from "./change-plan.mjs" + +export function checkSpaClientChanges(domain, spaConfig) { + const { packageName, port } = spaConfig + const callbackUrl = `http://localhost:${port}` + + return createChangeItem(ChangeAction.CREATE, { + resource: "SPA Client", + name: `${packageName}-spa`, + callbackUrl, + }) +} + +export async function applySpaClientChanges(changePlan) { + const spinner = ora(`Creating SPA Client: ${changePlan.name}`).start() + try { + const createArgs = [ + "apps", "create", + "--name", changePlan.name, + "--type", "spa", + "--auth-method", "none", + "--callbacks", changePlan.callbackUrl, + "--logout-urls", changePlan.callbackUrl, + "--origins", changePlan.callbackUrl, + "--web-origins", changePlan.callbackUrl, + "--json", + "--no-input", + ] + const { stdout } = await $({ timeout: 30000 })`auth0 ${createArgs}` + const client = JSON.parse(stdout) + spinner.succeed(`Created SPA Client: ${changePlan.name} (${client.client_id})`) + return client + } catch (e) { + spinner.fail("Failed to create SPA Client") + throw e + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/connections.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/connections.mjs new file mode 100644 index 0000000000..7bb3ea6f0c --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/connections.mjs @@ -0,0 +1,75 @@ +import ora from "ora" + +import { auth0ApiCall } from "./auth0-api.mjs" +import { ChangeAction, createChangeItem } from "./change-plan.mjs" + +export const DEFAULT_CONNECTION_NAME = "Username-Password-Authentication" + +export function checkDatabaseConnectionChanges(existingConnections, clientId) { + const existing = existingConnections.find((c) => c.name === DEFAULT_CONNECTION_NAME) + + if (!existing) { + return createChangeItem(ChangeAction.CREATE, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + enabledClients: [clientId], + }) + } + + const enabledClients = existing.enabled_clients || [] + if (!enabledClients.includes(clientId)) { + return createChangeItem(ChangeAction.UPDATE, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + existing, + summary: "Enable client on connection", + }) + } + + return createChangeItem(ChangeAction.SKIP, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + existing, + }) +} + +export async function applyDatabaseConnectionChanges(changePlan, clientId) { + if (changePlan.action === ChangeAction.SKIP) { + const spinner = ora(`Database Connection is up to date: ${changePlan.name}`).start() + spinner.succeed() + return changePlan.existing + } + + if (changePlan.action === ChangeAction.CREATE) { + const spinner = ora(`Creating Database Connection: ${DEFAULT_CONNECTION_NAME}`).start() + try { + const connectionData = { + strategy: "auth0", + name: DEFAULT_CONNECTION_NAME, + enabled_clients: [clientId], + } + const connection = await auth0ApiCall("post", "connections", connectionData) + spinner.succeed(`Created Database Connection: ${DEFAULT_CONNECTION_NAME}`) + return connection + } catch (e) { + spinner.fail("Failed to create Database Connection") + throw e + } + } + + if (changePlan.action === ChangeAction.UPDATE) { + const spinner = ora(`Updating Database Connection: ${DEFAULT_CONNECTION_NAME}`).start() + try { + const existing = changePlan.existing + const updatedClients = [...(existing.enabled_clients || []), clientId] + await auth0ApiCall("patch", `connections/${existing.id}`, { + enabled_clients: updatedClients, + }) + spinner.succeed(`Updated ${DEFAULT_CONNECTION_NAME}: enabled client ${clientId}`) + return { ...existing, enabled_clients: updatedClients } + } catch (e) { + spinner.fail("Failed to update Database Connection") + throw e + } + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/discovery.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/discovery.mjs new file mode 100644 index 0000000000..95986cc67c --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/discovery.mjs @@ -0,0 +1,55 @@ +import ora from "ora" + +import { auth0ApiCall } from "./auth0-api.mjs" +import { ChangeAction } from "./change-plan.mjs" +import { checkSpaClientChanges } from "./clients.mjs" +import { checkDatabaseConnectionChanges } from "./connections.mjs" + +export async function discoverExistingConnections() { + const spinner = ora("Discovering existing connections").start() + try { + const connections = (await auth0ApiCall("get", "connections")) || [] + spinner.succeed("Discovered existing connections") + return connections + } catch (e) { + const msg = e.message || String(e) + if (msg.includes("404") || msg.includes("Not Found")) { + spinner.succeed("No existing connections found") + return [] + } + spinner.fail("Failed to discover connections") + throw e + } +} + +export function buildChangePlan(connections, domain, spaConfig) { + const clientPlan = checkSpaClientChanges(domain, spaConfig) + const connectionPlan = checkDatabaseConnectionChanges(connections, "TO_BE_CREATED") + return { client: clientPlan, connection: connectionPlan } +} + +export function displayChangePlan(plan) { + console.log("\n Change Plan:\n") + + const items = [ + { name: "SPA Client", ...plan.client }, + { name: "Database Connection", ...plan.connection }, + ] + + for (const item of items) { + const icon = + item.action === ChangeAction.CREATE ? "+" : + item.action === ChangeAction.UPDATE ? "~" : "=" + const label = + item.action === ChangeAction.CREATE ? "CREATE" : + item.action === ChangeAction.UPDATE ? "UPDATE" : "SKIP " + + let detail = "" + if (item.summary) detail = ` (${item.summary})` + else if (item.callbackUrl) detail = ` (callback: ${item.callbackUrl})` + + console.log(` ${icon} [${label}] ${item.name || item.resource}${detail}`) + } + + console.log("") +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/env-writer.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/env-writer.mjs new file mode 100644 index 0000000000..199349d877 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/env-writer.mjs @@ -0,0 +1,32 @@ +import fs from "node:fs" +import ora from "ora" + +/** + * Write or update an .env file with the provided key-value pairs. + * Merges with existing content — preserves unrelated entries. + */ +export async function writeEnvFile(config, envFilePath) { + const spinner = ora("Writing .env").start() + + try { + let content = "" + if (fs.existsSync(envFilePath)) { + content = fs.readFileSync(envFilePath, "utf-8") + } + + for (const [key, value] of Object.entries(config)) { + const pattern = new RegExp(`^${key}=.*$`, "m") + if (pattern.test(content)) { + content = content.replace(pattern, `${key}=${value}`) + } else { + content += (content && !content.endsWith("\n") ? "\n" : "") + `${key}=${value}\n` + } + } + + fs.writeFileSync(envFilePath, content) + spinner.succeed(`Updated ${envFilePath}`) + } catch (e) { + spinner.fail("Failed to write .env") + throw e + } +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/helpers.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/helpers.mjs new file mode 100644 index 0000000000..308b4688ae --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/helpers.mjs @@ -0,0 +1,31 @@ +import readline from "node:readline/promises" +import { select } from "@inquirer/prompts" + +export async function confirmWithUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} (y/N): `) + rl.close() + + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes" +} + +export async function getInputFromUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} `) + rl.close() + + return answer.trim() +} + +export async function selectOptionFromList(message, options) { + const answer = await select({ message, choices: options }) + return answer +} diff --git a/main/.mintlify/skills/auth0-spa-js/scripts/utils/validation.mjs b/main/.mintlify/skills/auth0-spa-js/scripts/utils/validation.mjs new file mode 100644 index 0000000000..d06fdadc47 --- /dev/null +++ b/main/.mintlify/skills/auth0-spa-js/scripts/utils/validation.mjs @@ -0,0 +1,113 @@ +import fs from "node:fs" +import path from "node:path" + +import { $ } from "execa" +import ora from "ora" + +// --------------------------------------------------------------------------- +// Shared preflight checks +// --------------------------------------------------------------------------- + +export function checkNodeVersion() { + const [major] = process.versions.node.split(".").map(Number) + if (major < 20) { + console.error(`❌ Node.js 20+ required (found ${process.version})`) + process.exit(1) + } +} + +export async function checkAuth0CLI() { + const spinner = ora("Checking Auth0 CLI").start() + try { + await $`auth0 --version --no-input` + spinner.succeed("Auth0 CLI found") + } catch { + spinner.fail("Auth0 CLI not found") + console.error("\n Install Auth0 CLI: https://github.com/auth0/auth0-cli#installation") + console.error(" macOS: brew install auth0/auth0-cli/auth0") + console.error(" Linux: curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh") + console.error(" Windows: scoop install auth0\n") + process.exit(1) + } +} + +export async function getActiveTenant() { + const spinner = ora("Detecting active Auth0 tenant").start() + try { + const { stdout } = await $`auth0 tenants list --csv --no-input` + const lines = stdout.trim().split("\n").filter(Boolean) + // CSV format: domain,name,client_id — find the active one (marked with *) + // Fallback: use the first tenant + let domain = null + for (const line of lines) { + if (line.startsWith("*") || line.includes(",")) { + domain = line.replace(/^\*\s*/, "").split(",")[0].trim() + break + } + } + if (!domain) { + throw new Error("No active tenant found") + } + spinner.succeed(`Active tenant: ${domain}`) + return domain + } catch (e) { + spinner.fail("Failed to detect active tenant") + console.error("\n Run 'auth0 login' to authenticate with your Auth0 tenant\n") + process.exit(1) + } +} + +// --------------------------------------------------------------------------- +// SPA project validator +// --------------------------------------------------------------------------- + +export function validateSpaProject(projectPath) { + const spinner = ora("Validating SPA project").start() + + const pkgPath = path.join(projectPath, "package.json") + if (!fs.existsSync(pkgPath)) { + spinner.fail(`No package.json found in ${projectPath}`) + console.error("\n Please provide the path to your SPA project directory\n") + process.exit(1) + } + + let pkg + try { + pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) + } catch (e) { + spinner.fail("Failed to parse package.json") + process.exit(1) + } + + const deps = { ...pkg.dependencies, ...pkg.devDependencies } + + // Detect framework and default dev server port + let framework = "unknown" + let port = 5173 // default Vite port + + if (deps["react-scripts"]) { + framework = "react-cra" + port = 3000 + } else if (deps["vite"] && deps["svelte"]) { + framework = "svelte" + port = 5173 + } else if (deps["vite"] && (deps["solid-js"] || deps["solid"])) { + framework = "solid" + port = 5173 + } else if (deps["vite"]) { + framework = "vite" + port = 5173 + } else if (deps["@angular/core"]) { + framework = "angular" + port = 4200 + } else if (deps["vue"]) { + framework = "vue" + port = 5173 + } else if (deps["react"]) { + framework = "react" + port = 3000 + } + + spinner.succeed(`SPA project: ${pkg.name || "unnamed"} (${framework}, port ${port})`) + return { packageName: pkg.name || "app", framework, port } +} diff --git a/main/.mintlify/skills/auth0-springboot-api/SKILL.md b/main/.mintlify/skills/auth0-springboot-api/SKILL.md index 509300ef22..b4ed87300e 100644 --- a/main/.mintlify/skills/auth0-springboot-api/SKILL.md +++ b/main/.mintlify/skills/auth0-springboot-api/SKILL.md @@ -4,6 +4,10 @@ description: Use when securing Spring Boot API endpoints with JWT Bearer token v license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Spring Boot API Integration diff --git a/main/.mintlify/skills/auth0-springboot-api/references/api.md b/main/.mintlify/skills/auth0-springboot-api/references/api.md new file mode 100644 index 0000000000..2fbe6dfbd8 --- /dev/null +++ b/main/.mintlify/skills/auth0-springboot-api/references/api.md @@ -0,0 +1,306 @@ +# API Reference & Testing + +Complete reference for `com.auth0:auth0-springboot-api` configuration options and auto-configuration classes. + +--- + +## Configuration Reference + +### application.yml Properties + +```yaml +auth0: + domain: "your-tenant.auth0.com" # Required: Auth0 tenant domain (no https://) + audience: "https://api.example.com" # Required: API identifier / audience + dpop-mode: ALLOWED # Optional: DISABLED | ALLOWED | REQUIRED (default: ALLOWED) + dpop-iat-offset-seconds: 300 # Optional: DPoP proof time window (default: 300) + dpop-iat-leeway-seconds: 30 # Optional: DPoP proof time leeway (default: 30) +``` + +### application.properties Equivalent + +```properties +auth0.domain=your-tenant.auth0.com +auth0.audience=https://api.example.com +auth0.dpopMode=ALLOWED +auth0.dpopIatOffsetSeconds=300 +auth0.dpopIatLeewaySeconds=30 +``` + +### Environment Variables + +```bash +AUTH0_DOMAIN=your-tenant.auth0.com +AUTH0_AUDIENCE=https://api.example.com +AUTH0_DPOPMODE=ALLOWED +AUTH0_DPOPIATOFFSETSECONDS=300 +AUTH0_DPOPIATLEEWAYSECONDS=30 +``` + +> **Note:** Spring Boot environment variable binding removes dashes and is case-insensitive. Do not use underscores to separate words within a property name (e.g., use `AUTH0_DPOPMODE`, not `AUTH0_DPOP_MODE`). + +--- + +## Auth0Properties + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `domain` | `String` | Yes | — | Auth0 tenant domain. Format: `your-tenant.auth0.com` (no `https://` prefix) | +| `audience` | `String` | Yes | — | API Identifier from Auth0 Dashboard | +| `dpopMode` | `DPoPMode` | No | `ALLOWED` | Controls which token types are accepted | +| `dpopIatOffsetSeconds` | `Long` | No | `300` | Maximum age of DPoP proof `iat` claim in seconds | +| `dpopIatLeewaySeconds` | `Long` | No | `30` | Additional leeway for DPoP proof time validation | +| `domains` | `List` | No | — | Additional trusted Auth0 domains (for Multiple Custom Domains) | +| `cacheMaxEntries` | `Integer` | No | — | Maximum entries in the JWKS cache | +| `cacheTtlSeconds` | `Long` | No | — | TTL in seconds for JWKS cache entries | + +### Auto-Configuration Beans + +The SDK auto-configuration also supports custom beans: + +| Bean | Description | +|------|-------------| +| `DomainResolver` | Custom domain resolution for Multiple Custom Domains (MCD). Provide a `@Bean` of type `DomainResolver` to route requests to different Auth0 domains based on the request. | +| `AuthCache` | Custom cache implementation for JWKS or token verification results. Provide a `@Bean` of type `AuthCache` to override the default in-memory cache. | + +### DPoPMode Enum + +| Value | Description | +|-------|-------------| +| `DPoPMode.DISABLED` | Standard JWT Bearer only — rejects DPoP tokens | +| `DPoPMode.ALLOWED` | Accept both DPoP-bound and standard Bearer tokens (default) | +| `DPoPMode.REQUIRED` | Only accept DPoP-bound tokens — rejects standard Bearer | + +--- + +## Auto-Configuration Classes + +### Auth0AutoConfiguration + +Automatically creates `AuthOptions` and `AuthClient` beans from `Auth0Properties`. + +```java +// AuthOptions bean — built from application.yml +@Bean +public AuthOptions authOptions(Auth0Properties properties) { + AuthOptions.Builder builder = new AuthOptions.Builder() + .domain(properties.getDomain()) + .audience(properties.getAudience()); + + if (properties.getDpopMode() != null) { + builder.dpopMode(properties.getDpopMode()); + } + if (properties.getDpopIatLeewaySeconds() != null) { + builder.dpopIatLeewaySeconds(properties.getDpopIatLeewaySeconds()); + } + if (properties.getDpopIatOffsetSeconds() != null) { + builder.dpopIatOffsetSeconds(properties.getDpopIatOffsetSeconds()); + } + return builder.build(); +} + +// AuthClient bean — main entry point for verifying HTTP requests +@Bean +@ConditionalOnMissingBean +public AuthClient authClient(AuthOptions options) { + return AuthClient.from(options); +} +``` + +### Auth0SecurityAutoConfiguration + +Automatically creates the `Auth0AuthenticationFilter` bean. + +```java +@Bean +@ConditionalOnMissingBean +public Auth0AuthenticationFilter authAuthenticationFilter( + AuthClient authClient, Auth0Properties auth0Properties) { + return new Auth0AuthenticationFilter(authClient, auth0Properties); +} +``` + +### Auth0AuthenticationFilter + +A `OncePerRequestFilter` that: +1. Extracts the `Authorization` header +2. Calls `AuthClient.verifyRequest()` to validate the JWT (and DPoP proof if present) +3. Sets `Auth0AuthenticationToken` in the `SecurityContextHolder` +4. On failure, returns appropriate HTTP status and `WWW-Authenticate` header + +--- + +## Auth0AuthenticationToken + +Extends `AbstractAuthenticationToken`. Created after successful JWT validation. + +| Method | Return Type | Description | +|--------|-------------|-------------| +| `getName()` | `String` | User ID (`sub` claim from JWT) | +| `getClaims()` | `Map` | All JWT claims | +| `getClaim(String claimName)` | `Object` | Specific claim value, or `null` | +| `getScopes()` | `Set` | Parsed scopes from `scope` claim | +| `getAuthorities()` | `Collection` | `SCOPE_` prefixed authorities from JWT scopes | + +**Authority mapping:** The `scope` claim `"read:data write:data"` becomes authorities `SCOPE_read:data` and `SCOPE_write:data`. If no scopes are present, a default `ROLE_USER` authority is assigned. + +--- + +## Claims Reference + +### Standard JWT Claims + +| Claim | Description | Access | +|-------|-------------|--------| +| `sub` | User ID (subject) | `authentication.getName()` or `token.getClaim("sub")` | +| `scope` | Space-separated scopes | `token.getScopes()` or `token.getClaim("scope")` | +| `aud` | Audience (API identifier) | `token.getClaim("aud")` | +| `iss` | Issuer (Auth0 tenant URL) | `token.getClaim("iss")` | +| `exp` | Expiration timestamp | `token.getClaim("exp")` | +| `iat` | Issued-at timestamp | `token.getClaim("iat")` | + +### Auth0-Specific Claims + +| Claim | Description | +|-------|-------------| +| `permissions` | Array of RBAC permissions (if Enable RBAC is on) | +| `email` | User email (if requested in scope) | +| `https://your-domain.com/*` | Custom claims added via Auth0 Actions (namespaced) | + +--- + +## Complete Minimal Example + +```java +// src/main/java/com/example/SecurityConfig.java +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + SecurityFilterChain apiSecurity( + HttpSecurity http, + Auth0AuthenticationFilter authFilter + ) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/public").permitAll() + .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin") + .anyRequest().authenticated()) + .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} + +// src/main/java/com/example/ApiController.java +@RestController +@RequestMapping("/api") +public class ApiController { + + @GetMapping("/public") + public ResponseEntity> publicEndpoint() { + return ResponseEntity.ok(Map.of("message", "Public endpoint")); + } + + @GetMapping("/protected") + public ResponseEntity> protectedEndpoint(Authentication authentication) { + Auth0AuthenticationToken token = (Auth0AuthenticationToken) authentication; + return ResponseEntity.ok(Map.of( + "user", authentication.getName(), + "scopes", token.getScopes() + )); + } + + @GetMapping("/admin/dashboard") + public ResponseEntity> adminEndpoint(Authentication authentication) { + return ResponseEntity.ok(Map.of( + "message", "Admin access granted", + "user", authentication.getName() + )); + } +} +``` + +```yaml +# src/main/resources/application.yml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" + +spring: + application: + name: auth0-api +``` + +--- + +## Testing Checklist + +1. **Public endpoint returns 200 without token:** + ```bash + curl http://localhost:8080/api/public + ``` + +2. **Protected endpoint returns 401 without token:** + ```bash + curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/protected + # Expected: 401 + ``` + +3. **Protected endpoint returns 200 with valid token:** + ```bash + curl http://localhost:8080/api/protected \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + ``` + +4. **Scope-protected endpoint returns 403 with insufficient scope:** + ```bash + curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/admin/dashboard \ + -H "Authorization: Bearer TOKEN_WITHOUT_ADMIN_SCOPE" + # Expected: 403 + ``` + +5. **DPoP token accepted (if dpop-mode is ALLOWED or REQUIRED):** + ```bash + curl http://localhost:8080/api/protected \ + -H "Authorization: DPoP YOUR_DPOP_TOKEN" \ + -H "DPoP: YOUR_DPOP_PROOF" + ``` + +--- + +## Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| 401 `invalid_token` | Audience mismatch | Verify `auth0.audience` matches API Identifier exactly | +| 401 `invalid_issuer` | Domain has `https://` prefix | Use `your-tenant.auth0.com` format only | +| 403 Forbidden | Token missing required scope | Request token with correct scopes; check `hasAuthority` values | +| No `Auth0AuthenticationFilter` bean | Missing auto-configuration | Ensure `auth0-springboot-api` is on classpath and `auth0.domain`/`auth0.audience` are set | +| DPoP `invalid_dpop_proof` | Proof validation failed | Check DPoP proof format, `iat` claim within time window | +| Token expired | Short-lived test token | Request a fresh token from Auth0 Dashboard or CLI | +| Multiple Authorization headers | Duplicate header sent | Send exactly one `Authorization` header per request | + +--- + +## Security Considerations + +- **No client secret needed** — This library validates JWTs via JWKS (public key), not client credentials +- **Never hardcode domain or audience** — Use `application.yml` or environment variables +- **Use HTTPS in production** — Auth0 requires HTTPS for token issuance; API should also use HTTPS +- **Stateless sessions** — Always configure `SessionCreationPolicy.STATELESS` for API endpoints +- **Use minimal scopes** — Only enforce scopes your API actually needs +- **Keep packages updated** — Regularly update `auth0-springboot-api` for security patches +- **DPoP for high-security APIs** — Enable `dpop-mode: REQUIRED` to prevent token theft + +--- + +## References + +- [Auth0 Java Spring Security API Quickstart](https://auth0.com/docs/quickstart/backend/java-spring-security5) +- [SDK GitHub Repository](https://github.com/auth0/auth0-auth-java) +- [Spring Security Documentation](https://docs.spring.io/spring-security/reference/) +- [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) diff --git a/main/.mintlify/skills/auth0-springboot-api/references/integration.md b/main/.mintlify/skills/auth0-springboot-api/references/integration.md new file mode 100644 index 0000000000..c8145869ec --- /dev/null +++ b/main/.mintlify/skills/auth0-springboot-api/references/integration.md @@ -0,0 +1,418 @@ +# Auth0 Spring Boot API Integration Patterns + +Advanced integration patterns for Spring Boot API applications using `auth0-springboot-api`. + +--- + +## Scope-Based Authorization + +The library maps JWT scopes to Spring Security authorities with a `SCOPE_` prefix. A token with `scope: "read:messages write:messages"` produces authorities `SCOPE_read:messages` and `SCOPE_write:messages`. + +### Option 1: Security Filter Chain (Recommended) + +Define scope requirements in your security configuration: + +```java +@Configuration +public class SecurityConfig { + + @Bean + SecurityFilterChain apiSecurity( + HttpSecurity http, + Auth0AuthenticationFilter authFilter + ) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/public").permitAll() + .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin") + .requestMatchers("/api/users/**").hasAuthority("SCOPE_read:users") + .anyRequest().authenticated()) + .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} +``` + +### Option 2: Method-Level Security with @PreAuthorize + +Requires `@EnableMethodSecurity` on a configuration class: + +```java +@Configuration +@EnableMethodSecurity +public class MethodSecurityConfig { + // Enables @PreAuthorize annotations +} +``` + +```java +@RestController +@RequestMapping("/api/users") +public class UserManagementController { + + @GetMapping + @PreAuthorize("hasAuthority('SCOPE_read:users')") + public ResponseEntity> getUsers() { + return ResponseEntity.ok(userService.getAllUsers()); + } + + @PostMapping + @PreAuthorize("hasAuthority('SCOPE_write:users')") + public ResponseEntity createUser(@RequestBody User user) { + return ResponseEntity.ok(userService.createUser(user)); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasAuthority('SCOPE_delete:users')") + public ResponseEntity deleteUser(@PathVariable String id) { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } +} +``` + +### Option 3: Programmatic Scope Check + +Use `getScopes()` on the token for custom logic: + +```java +@GetMapping("/admin") +public ResponseEntity> adminEndpoint(Authentication authentication) { + if (authentication instanceof Auth0AuthenticationToken auth0Token) { + Set scopes = auth0Token.getScopes(); + + if (!scopes.contains("admin") || !scopes.contains("read:admin")) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(Map.of("error", "insufficient_scope")); + } + + return ResponseEntity.ok(Map.of("message", "Admin access granted")); + } + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); +} +``` + +### Define Permissions in Auth0 + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Click the **Permissions** tab +4. Add permissions matching your scope names (e.g., `read:users`, `write:users`) + +### Request Tokens with Scopes + +```bash +curl -X POST https://your-tenant.auth0.com/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "audience": "https://my-springboot-api", + "grant_type": "client_credentials", + "scope": "read:users write:users" + }' +``` + +--- + +## DPoP Authentication + +[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) binds tokens to a specific client key pair, preventing token theft. + +### Configuration Modes + +#### ALLOWED Mode (Default) + +Accepts both Bearer and DPoP tokens: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" + dpop-mode: ALLOWED +``` + +#### REQUIRED Mode + +Only accepts DPoP tokens — rejects standard Bearer: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" + dpop-mode: REQUIRED +``` + +#### DISABLED Mode + +Standard JWT Bearer only — rejects DPoP tokens: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" + dpop-mode: DISABLED +``` + +### Fine-Tuning DPoP Time Validation (Optional) + +The defaults work for most use cases. Only adjust these if you need to handle clock skew or network delays: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" + dpop-mode: ALLOWED + dpop-iat-offset-seconds: 300 # Optional: max age of DPoP proof (default: 300) + dpop-iat-leeway-seconds: 30 # Optional: additional time leeway (default: 30) +``` + +### How DPoP Works in Controllers + +DPoP validation is handled by the `Auth0AuthenticationFilter` before the request reaches your controller. Your controller code is the same regardless of whether the client used Bearer or DPoP: + +```java +@GetMapping("/sensitive") +public ResponseEntity> sensitiveEndpoint(Authentication authentication) { + // Works the same for both Bearer and DPoP tokens + if (authentication instanceof Auth0AuthenticationToken auth0Token) { + return ResponseEntity.ok(Map.of( + "user", authentication.getName(), + "scopes", auth0Token.getScopes(), + "message", "Access granted" + )); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); +} +``` + +### DPoP WWW-Authenticate Headers + +The library automatically generates RFC-compliant `WWW-Authenticate` headers on failures: + +```http +# ALLOWED mode (default) +WWW-Authenticate: Bearer realm="api", DPoP algs="ES256" + +# REQUIRED mode +WWW-Authenticate: DPoP algs="ES256" + +# DPoP-specific errors +WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="DPoP proof validation failed" +``` + +### Enable DPoP on Auth0 API + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Enable DPoP binding requirement + +--- + +## Accessing User Claims + +### From Controller Parameter + +```java +@GetMapping("/profile") +public ResponseEntity> getUserProfile(Authentication authentication) { + if (authentication instanceof Auth0AuthenticationToken auth0Token) { + return ResponseEntity.ok(Map.of( + "sub", String.valueOf(auth0Token.getClaim("sub")), + "email", String.valueOf(auth0Token.getClaim("email")), + "scope", String.valueOf(auth0Token.getClaim("scope")), + "scopes", auth0Token.getScopes() + )); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); +} +``` + +### Common JWT Claims + +| Claim | Description | +|-------|-------------| +| `sub` | User ID (subject) | +| `scope` | Space-separated list of granted scopes | +| `aud` | Audience (your API identifier) | +| `iss` | Issuer (your Auth0 tenant URL) | +| `exp` | Expiration timestamp | +| `iat` | Issued-at timestamp | + +Custom claims added via Auth0 Actions use namespaced keys, e.g., `https://your-domain.com/role`. + +--- + +## Error Handling + +### BaseAuthException Hierarchy + +The library uses `BaseAuthException` subclasses for different error conditions: + +| Exception | HTTP Status | Cause | +|-----------|-------------|-------| +| `MissingAuthorizationException` | 400 | No or multiple `Authorization` headers | +| `VerifyAccessTokenException` | 401 | JWT validation failed (expired, bad signature, wrong audience) | +| `InvalidAuthSchemeException` | 400 | Wrong auth scheme for configured DPoP mode | +| `InvalidDpopProofException` | 400 | DPoP proof validation failed | +| `InsufficientScopeException` | 403 | Valid token but missing required scope | + +The `Auth0AuthenticationFilter` handles all exceptions automatically, setting the appropriate HTTP status and `WWW-Authenticate` header. No custom exception handling is needed in controllers for auth errors. + +### Custom Error Responses + +For non-auth errors in your controllers, use standard Spring patterns: + +```java +@ExceptionHandler(Exception.class) +public ResponseEntity> handleError(Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", e.getMessage())); +} +``` + +### Standard Error Responses + +| Status | Cause | Fix | +|--------|-------|-----| +| 401 | Missing or invalid token | Include valid `Authorization: Bearer ` header | +| 401 | Expired token | Request a fresh access token | +| 401 | Wrong audience | Token's `aud` claim must match your API Identifier | +| 403 | Insufficient scope | Token must include required scopes | + +--- + +## Mixed Public and Protected Endpoints + +```java +@RestController +@RequestMapping("/api") +public class MixedController { + + // Public - no auth needed + @GetMapping("/public") + public ResponseEntity> publicEndpoint() { + return ResponseEntity.ok(Map.of("message", "Public endpoint")); + } + + // Protected - requires valid JWT + @GetMapping("/private") + public ResponseEntity> privateEndpoint(Authentication authentication) { + return ResponseEntity.ok(Map.of( + "message", "Private endpoint", + "user", authentication.getName() + )); + } + + // Protected with scope + @GetMapping("/messages") + @PreAuthorize("hasAuthority('SCOPE_read:messages')") + public ResponseEntity> messagesEndpoint() { + return ResponseEntity.ok(Map.of("messages", List.of("Hello", "World"))); + } +} +``` + +--- + +## CORS Configuration + +For APIs consumed by browser-based SPAs, configure CORS **before** the auth filter: + +```java +@Configuration +public class SecurityConfig { + + @Bean + SecurityFilterChain apiSecurity( + HttpSecurity http, + Auth0AuthenticationFilter authFilter + ) throws Exception { + return http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/public").permitAll() + .anyRequest().authenticated()) + .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:3000")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type", "DPoP")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", config); + return source; + } +} +``` + +--- + +## Testing + +### Integration Testing with MockMvc + +```java +@SpringBootTest +@AutoConfigureMockMvc +class ApiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void publicEndpoint_returns200() throws Exception { + mockMvc.perform(get("/api/public")) + .andExpect(status().isOk()); + } + + @Test + void protectedEndpoint_withoutToken_returns401() throws Exception { + mockMvc.perform(get("/api/protected")) + .andExpect(status().isUnauthorized()); + } +} +``` + +### Testing with curl + +```bash +# Get a test token +TOKEN=$(auth0 test token --audience https://my-springboot-api --json | jq -r '.access_token') + +# Test protected endpoint +curl http://localhost:8080/api/protected \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## Security Considerations + +- **Stateless sessions** — Always use `SessionCreationPolicy.STATELESS` for API endpoints +- **No client secret** — This library validates JWTs via JWKS; no client secret is stored or needed +- **CORS before auth** — Configure CORS middleware before the auth filter in the security chain +- **Use HTTPS in production** — Auth0 requires HTTPS for token issuance +- **Minimal scopes** — Only enforce scopes your API actually needs +- **DPoP for high-security** — Enable `dpop-mode: REQUIRED` for APIs handling sensitive data + +--- + +## References + +- [API Reference](api.md) +- [Setup Guide](setup.md) +- [Main Skill](../SKILL.md) diff --git a/main/.mintlify/skills/auth0-springboot-api/references/setup.md b/main/.mintlify/skills/auth0-springboot-api/references/setup.md new file mode 100644 index 0000000000..e9e87870bd --- /dev/null +++ b/main/.mintlify/skills/auth0-springboot-api/references/setup.md @@ -0,0 +1,228 @@ +# Auth0 Spring Boot API Setup Guide + +Setup instructions for Spring Boot API applications using `auth0-springboot-api`. + +--- + +## Auth0 Configuration + +> **Agent instruction:** +> +> **Check if Auth0 domain and audience are already in the user's prompt first.** +> If the prompt contains Auth0 domain and audience, use them directly — skip to "Write Configuration" below. Do NOT call `AskUserQuestion` to re-confirm. +> +> **If Auth0 configuration is NOT provided**, use `AskUserQuestion` to ask: +> "How would you like to configure Auth0?" +> - Option A: "Automatic setup using Auth0 CLI (recommended)" +> - Option B: "Manual setup" — provide domain and audience manually +> +> **If Automatic Setup:** +> +> 1. **Pre-flight checks:** +> - Verify Auth0 CLI is installed: `auth0 --version` +> - Verify logged in: `auth0 tenants list --csv --no-input` +> - If any check fails, guide user to install/login, or fall back to manual setup +> +> 2. **Create the API resource using Auth0 CLI:** +> ```bash +> auth0 apis create --name "My Spring Boot API" --identifier https://my-springboot-api --json +> ``` +> Then write the returned domain and audience to `application.yml`. +> +> **If Manual Setup:** +> +> Ask the user for: +> - Auth0 Domain (e.g., `your-tenant.auth0.com`) +> - API Audience / Identifier (e.g., `https://my-springboot-api`) +> +> Write the configuration file with provided values. + +--- + +## Quick Setup (Automated) + +Uses the Auth0 CLI to create an Auth0 API resource and configure your project. + +### Step 1: Install Auth0 CLI and create API resource + +```bash +# Install Auth0 CLI (macOS) +brew install auth0/auth0-cli/auth0 + +# Login +auth0 login --no-input + +# Create an Auth0 API resource +auth0 apis create \ + --name "My Spring Boot API" \ + --identifier https://my-springboot-api \ + --json +``` + +Note the `identifier` value — this is your Audience. + +### Step 2: Get your domain + +```bash +auth0 tenants list +``` + +Your domain is shown in the output (e.g., `your-tenant.auth0.com`). + +### Step 3: Write configuration + +Add to `src/main/resources/application.yml`: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" +``` + +Or `src/main/resources/application.properties`: + +```properties +auth0.domain=your-tenant.auth0.com +auth0.audience=https://my-springboot-api +``` + +--- + +## Manual Setup + +### Install Dependency + +**Gradle (build.gradle):** + +```groovy +implementation 'com.auth0:auth0-springboot-api:1.0.0-beta.1' +``` + +**Maven (pom.xml):** + +```xml + + com.auth0 + auth0-springboot-api + 1.0.0-beta.1 + +``` + +### Create Auth0 API Resource + +1. Go to Auth0 Dashboard → Applications → APIs +2. Click **Create API** +3. Set a **Name** and an **Identifier** (e.g., `https://my-springboot-api`) +4. Note the Identifier — this is your `audience` + +### Configure application.yml + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" +``` + +**Important:** Domain format is `your-tenant.auth0.com` — do NOT include `https://`. + +### Get Auth0 Configuration + +- **Domain:** Auth0 Dashboard → Settings → Domain (or `auth0 tenants list`) +- **Audience:** The identifier you set when creating the API resource + +--- + +## Post-Setup Steps + +1. **Verify audience matches** — The `auth0.audience` value must exactly match your API Identifier in Auth0 Dashboard +2. **Add SecurityConfig** — Create a `SecurityConfig.java` class with `Auth0AuthenticationFilter` added before `UsernamePasswordAuthenticationFilter` +3. **Build and test** — Run `./gradlew bootRun` (or `./mvnw spring-boot:run`) and test endpoints + +--- + +## Environment-Specific Configuration + +This library validates JWTs via JWKS (public key verification). **No client secret is needed.** + +The `domain` and `audience` values are not secrets — they are public identifiers. However, they typically differ per environment: + +### Development + +Use `application.yml` or `application.properties` directly: + +```yaml +auth0: + domain: "your-tenant.auth0.com" + audience: "https://my-springboot-api" +``` + +### Production + +Use environment variables (override `application.yml`): + +```bash +export AUTH0_DOMAIN=your-tenant.auth0.com +export AUTH0_AUDIENCE=https://my-springboot-api +``` + +Or use Spring profiles (`application-prod.yml`). + +--- + +## Getting a Test Token + +### Via Auth0 Dashboard + +1. Go to Auth0 Dashboard → Applications → APIs +2. Select your API +3. Click the **Test** tab +4. Click **Copy Token** to get a test access token + +### Via Auth0 CLI (Client Credentials) + +```bash +auth0 test token \ + --audience https://my-springboot-api +``` + +### Via curl (Client Credentials Flow) + +```bash +curl -X POST https://your-tenant.auth0.com/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "audience": "https://my-springboot-api", + "grant_type": "client_credentials" + }' +``` + +--- + +## Verification + +1. Application starts without errors: `./gradlew bootRun` +2. Public endpoint accessible without token: `curl http://localhost:8080/api/public` +3. Protected endpoint returns 401 without token: `curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/protected` +4. Protected endpoint returns 200 with valid token + +--- + +## Troubleshooting + +**401 Unauthorized - "invalid_token":** Verify that the `auth0.audience` in config exactly matches your API Identifier in Auth0 Dashboard. + +**401 Unauthorized - "invalid_issuer":** Ensure `auth0.domain` does not include `https://` — use `your-tenant.auth0.com` format only. + +**No Auth0AuthenticationFilter bean found:** Ensure `auth0-springboot-api` dependency is on the classpath and both `auth0.domain` and `auth0.audience` are configured. + +**Token expired:** Test tokens from the Dashboard are short-lived. Request a fresh token. + +--- + +## Next Steps + +- [Integration Guide](integration.md) +- [API Reference](api.md) +- [Main Skill](../SKILL.md) diff --git a/main/.mintlify/skills/auth0-swift/SKILL.md b/main/.mintlify/skills/auth0-swift/SKILL.md index 7d325393a3..b056acd569 100644 --- a/main/.mintlify/skills/auth0-swift/SKILL.md +++ b/main/.mintlify/skills/auth0-swift/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding Auth0 authentication to an iOS, macOS, tvOS, watchO license: Proprietary metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Swift Integration @@ -250,6 +254,13 @@ private let auth = AuthenticationService() | Not calling `clearSession()` on logout | Always call `clearSession()` to remove the Auth0 session cookie from the browser | | Build error "No such module 'Auth0'" | Verify the package is added to the correct target; for CocoaPods, open `.xcworkspace` | +## Related Skills + +- `auth0-quickstart` - Basic Auth0 setup +- `auth0-cli` - Manage Auth0 resources from the terminal + +--- + ## References - [Auth0.swift GitHub](https://github.com/auth0/Auth0.swift) diff --git a/main/.mintlify/skills/auth0-swift/references/api.md b/main/.mintlify/skills/auth0-swift/references/api.md new file mode 100644 index 0000000000..9143d08f0d --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/references/api.md @@ -0,0 +1,192 @@ +# API Reference & Testing — Auth0 Swift + +## Configuration Reference + +### Auth0.plist Keys + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `ClientId` | String | Yes | Your Auth0 application Client ID | +| `Domain` | String | Yes | Your Auth0 tenant domain (e.g., `tenant.auth0.com`) | + +### Programmatic Initialization + +Use when you cannot use `Auth0.plist` (e.g., reading credentials from environment): + +```swift +// Web Auth with explicit credentials +Auth0 + .webAuth(clientId: "YOUR_CLIENT_ID", domain: "YOUR_DOMAIN") + .start() + +// Authentication API with explicit credentials +Auth0 + .authentication(clientId: "YOUR_CLIENT_ID", domain: "YOUR_DOMAIN") + .login(usernameOrEmail: "user@example.com", password: "password", + realmOrConnection: "Username-Password-Authentication", + scope: "openid profile email") + +// CredentialsManager with explicit credentials +let authentication = Auth0.authentication(clientId: "YOUR_CLIENT_ID", domain: "YOUR_DOMAIN") +let credentialsManager = CredentialsManager(authentication: authentication) +``` + +### WebAuth Builder Options + +| Method | Type | Description | +|--------|------|-------------| +| `.useHTTPS()` | — | Use Universal Links (HTTPS) for callback — recommended | +| `.scope(_ scope: String)` | `String` | Space-separated OAuth scopes. Default: `"openid profile email"`. Add `"offline_access"` for refresh tokens | +| `.audience(_ audience: String)` | `String` | API audience (resource identifier). Required for API access tokens | +| `.parameters(_ params: [String: String])` | `[String: String]` | Additional authorize parameters (e.g., `["screen_hint": "signup"]`) | +| `.organization(_ organization: String)` | `String` | Auth0 Organization ID or name | +| `.invitationURL(_ url: URL)` | `URL` | Accept an organization invitation | +| `.redirectURL(_ url: URL)` | `URL` | Override the callback URL | +| `.provider(_ provider: WebAuthProvider)` | — | Use SFSafariViewController or custom provider | +| `.ephemeralSession()` | — | Do not persist session cookies (no SSO) | +| `.nonce(_ nonce: String)` | `String` | Override the auto-generated nonce | +| `.maxAge(_ maxAge: Int)` | `Int` | Maximum age (seconds) of allowed authentication | +| `.leeway(_ leeway: Int)` | `Int` | Clock skew tolerance in seconds for ID token validation | + +### CredentialsManager Options + +| Method | Type | Description | +|--------|------|-------------| +| `CredentialsManager(authentication:)` | — | Standard initialization | +| `CredentialsManager(authentication:maxRetries:)` | `Int` | Set retry attempts on transient errors | +| `CredentialsManager(authentication:storeKey:)` | `String` | Custom Keychain key for multi-account support | +| `.store(credentials:)` | `Bool` | Store credentials; returns `false` if Keychain write fails | +| `.credentials()` | `Credentials` (async) | Retrieve valid credentials; auto-renews if expired | +| `.credentials(minTTL:)` | `Credentials` (async) | Retrieve with minimum remaining TTL | +| `.canRenew()` | `Bool` | Returns `true` if a refresh token is available | +| `.hasValid(minTTL:)` | `Bool` | Returns `true` if access token is valid for at least `minTTL` seconds | +| `.clear()` | `Bool` | Remove credentials from Keychain | +| `.revoke(headers:)` | `Void` (async) | Revoke refresh token and clear credentials | +| `.enableBiometrics(withTitle:)` | — | Prompt biometric authentication when retrieving credentials | +| `.enableBiometrics(withTitle:policy:)` | — | Biometrics with custom `LAPolicy` | +| `.clearBiometricSession()` | — | Clear cached biometric session | +| `.isBiometricSessionValid()` | `Bool` | Check if biometric session is still valid | + +### Biometric Policy Options + +| Policy | Description | +|--------|-------------| +| `.default` | System manages prompts; allows reuse within a short window | +| `.always` | Fresh biometric prompt every time credentials are retrieved | +| `.session(timeoutInSeconds:)` | Reuse biometric auth for specified seconds (default 300) | +| `.appLifecycle(timeoutInSeconds:)` | Reuse for app lifecycle (default 3600 seconds / 1 hour) | + +### Credentials Object + +| Property | Type | Description | +|----------|------|-------------| +| `accessToken` | `String` | JWT access token for API calls | +| `tokenType` | `String` | Token type, usually `"Bearer"` | +| `idToken` | `String` | JWT ID token with user identity claims | +| `refreshToken` | `String?` | Refresh token (requires `offline_access` scope) | +| `expiresIn` | `Date` | Access token expiration date | +| `scope` | `String?` | Granted scopes | + +--- + +## Claims Reference + +### Standard OIDC Claims (from ID Token) + +| Claim | Type | Description | +|-------|------|-------------| +| `sub` | String | User ID (e.g., `"auth0|64abc123"`) | +| `name` | String | Full display name | +| `given_name` | String | First name | +| `family_name` | String | Last name | +| `email` | String | Email address | +| `email_verified` | Bool | Whether email is verified | +| `picture` | String | Profile picture URL | +| `updated_at` | Date | Last profile update timestamp | +| `iss` | String | Issuer — your Auth0 domain | +| `aud` | String | Audience — your Client ID | +| `exp` | Date | Expiration time | +| `iat` | Date | Issued at time | + +### Auth0-Specific Claims + +| Claim | Type | Description | +|-------|------|-------------| +| `https://example.com/permissions` | `[String]` | User permissions (added via Auth0 Actions) | +| `https://example.com/roles` | `[String]` | User roles (added via Auth0 Actions) | +| `org_id` | String | Organization ID | +| `org_name` | String | Organization name | + +### Decoding Claims + +```swift +import Auth0 + +// Decode ID token claims +if let claims = try? IDTokenClaimsValidation().validate(credentials.idToken) { + print("User ID: \(claims.subject)") + print("Email: \(claims.email ?? "none")") +} + +// Or decode manually with JWT libraries +// The ID token is a standard JWT — decode payload with any JWT library +``` + +--- + +## Testing Checklist + +> **Physical device note:** Web Auth (ASWebAuthenticationSession) works in the iOS Simulator, but biometric authentication (Face ID / Touch ID) requires a real device. Test biometric flows on a physical device before shipping. Simulator has limitations for camera-based Face ID and some Keychain access control scenarios. + +- [ ] `Auth0.plist` is present in the Xcode project and added to the app target +- [ ] Both `https://` Universal Link and `{bundle}://` custom scheme URLs are in Auth0 Dashboard Callback URLs +- [ ] App builds without errors: `xcodebuild build -scheme SCHEME -destination "platform=iOS Simulator,name=iPhone 16"` +- [ ] Login opens system browser (ASWebAuthenticationSession) and redirects back to app +- [ ] `credentialsManager.store(credentials:)` returns `true` after login +- [ ] `credentialsManager.canRenew()` returns `true` after storing credentials with `offline_access` +- [ ] `credentialsManager.credentials()` returns valid access token without re-login (token auto-refresh) +- [ ] Logout clears session cookie (subsequent login shows login prompt, not silent SSO) +- [ ] `credentialsManager.clear()` returns `true` after logout +- [ ] Error cases are handled: `userCancelled`, `noCredentialsAvailable`, `failedToRenewCredentials` +- [ ] Biometric prompt appears (if enabled) before credentials are returned +- [ ] App state persists across launches (credentials survive app restart) + +--- + +## Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| `Auth0.plist not found` | File not added to target | Right-click `Auth0.plist` → Add Files → check app target | +| `No such module 'Auth0'` | Package not installed or wrong target | Verify SPM package in Xcode → Package Dependencies; re-resolve | +| `Redirect to app fails` | Callback URL mismatch | Ensure URL in Auth0 Dashboard matches bundle ID exactly | +| `Cannot open URL` (iOS) | Missing URL scheme | Add `$(PRODUCT_BUNDLE_IDENTIFIER)` to URL Schemes in Info tab | +| Login shows blank screen | Universal Links not configured | Use `.useHTTPS()` only if Universal Links are configured, else omit it | +| Token not renewable | Missing `offline_access` scope | Add `"offline_access"` to `.scope()` call | +| `biometricsFailed` error | No biometric enrolled or cancelled | Fall back to re-login | +| `cannotAccessKeychainItem` | Keychain entitlements missing | Verify app has Keychain Sharing capability or correct entitlements | +| Crash on macOS | Missing network entitlement | Add "Outgoing Connections (Client)" capability in Signing & Capabilities | +| Build fails on Swift 6 | Concurrency issues | Ensure callbacks are dispatched on `@MainActor` for UI updates | + +--- + +## Security Considerations + +- **No client secret**: Native apps use PKCE (Proof Key for Code Exchange) — no client secret is required or used. Do not add a client secret to `Auth0.plist`. +- **Keychain storage**: Always use `CredentialsManager` for token storage. Never use `UserDefaults` or plain files. Tokens in `UserDefaults` are readable by other apps on jailbroken devices. +- **Universal Links vs custom scheme**: Universal Links (`https://`) are recommended for production as they cannot be intercepted by malicious apps. Custom schemes (`{bundle}://`) are acceptable but less secure. +- **Scope minimization**: Request only the scopes your app needs. Avoid requesting permissions you do not use. +- **Refresh token rotation**: Enable Refresh Token Rotation in Auth0 Dashboard (Applications → Advanced Settings → OAuth) to detect token theft. +- **Biometric storage**: When using `enableBiometrics()`, the Keychain entry uses `kSecAccessControlBiometryCurrentSet` which invalidates the entry if new biometrics are enrolled — protecting against biometric spoofing. +- **Certificate pinning**: For extra security, use a custom `URLSession` with certificate pinning when calling your API with the access token. +- **App Transport Security**: Ensure `NSAllowsArbitraryLoads` is not set to `true` in production builds. + +--- + +## Related Skills + +- [auth0-android](/auth0-android) — Auth0 authentication for Android/Kotlin apps +- [auth0-flutter](/auth0-flutter) — Cross-platform iOS + Android authentication with Flutter +- [auth0-react-native](/auth0-react-native) — Cross-platform iOS + Android authentication with React Native +- [auth0-quickstart](/auth0-quickstart) — Set up an Auth0 account and application +- [auth0-mfa](/auth0-mfa) — Configure multi-factor authentication diff --git a/main/.mintlify/skills/auth0-swift/references/integration.md b/main/.mintlify/skills/auth0-swift/references/integration.md new file mode 100644 index 0000000000..dc2375241e --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/references/integration.md @@ -0,0 +1,462 @@ +# Integration Patterns — Auth0 Swift + +## Authentication Flow + +```text +User taps "Log In" + ↓ +Auth0.webAuth().start() + ↓ +ASWebAuthenticationSession opens Auth0 Universal Login + ↓ (user authenticates) +Auth0 redirects to {bundle}:// or https:// callback + ↓ +SDK exchanges code for tokens (PKCE) + ↓ +Credentials returned (accessToken, idToken, refreshToken) + ↓ +credentialsManager.store(credentials:) → Keychain +``` + +--- + +## Web Auth Login & Logout + +### Basic Login (Async/Await) + +```swift +import Auth0 + +func login() async throws -> Credentials { + return try await Auth0 + .webAuth() + .useHTTPS() // Use Universal Links callback + .scope("openid profile email offline_access") + .start() +} +``` + +### Basic Login (Completion Handler) + +```swift +Auth0 + .webAuth() + .useHTTPS() + .scope("openid profile email offline_access") + .start { result in + switch result { + case .success(let credentials): + // Access token available at credentials.accessToken + case .failure(let error): + print("Login failed: \(error)") + } + } +``` + +### Logout + +```swift +// Step 1: Clear the Auth0 session cookie (prevents silent re-login) +try await Auth0 + .webAuth() + .useHTTPS() + .clearSession() + +// Step 2: Clear locally stored credentials +let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) +_ = credentialsManager.clear() +``` + +### Sign Up (Direct to Registration Screen) + +```swift +try await Auth0 + .webAuth() + .useHTTPS() + .parameters(["screen_hint": "signup"]) + .start() +``` + +### Custom Scopes and Audience + +```swift +// Request an access token for your API +try await Auth0 + .webAuth() + .useHTTPS() + .audience("https://your-api.example.com") + .scope("openid profile email offline_access read:data") + .start() +``` + +### Ephemeral Session (No SSO, No Cookie Persistence) + +```swift +// Each login shows the login page — no session cookie stored +try await Auth0 + .webAuth() + .useHTTPS() + .ephemeralSession() + .start() +``` + +--- + +## CredentialsManager + +`CredentialsManager` handles secure Keychain storage and automatic token refresh. + +### Basic Setup + +```swift +// Initialize once (e.g., as a property on your auth service) +let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) +``` + +### Store After Login + +```swift +let credentials = try await Auth0.webAuth().start() +guard credentialsManager.store(credentials: credentials) else { + throw AuthError.keychainWriteFailed +} +``` + +### Retrieve (Auto-Refreshes Expired Tokens) + +```swift +do { + let credentials = try await credentialsManager.credentials() + // credentials.accessToken is guaranteed to be valid + callAPI(with: credentials.accessToken) +} catch CredentialsManagerError.noCredentialsAvailable { + // No credentials stored — show login screen + await showLogin() +} catch CredentialsManagerError.failedToRenewCredentials(let error) { + // Refresh token expired or revoked — force re-login + _ = credentialsManager.clear() + await showLogin() +} +``` + +### Check Authentication State on Launch + +```swift +func checkSession() -> Bool { + // Returns true if a valid refresh token is stored + return credentialsManager.canRenew() +} + +// Check if access token is still valid without auto-refresh +func hasValidToken(minTTL: Int = 60) -> Bool { + return credentialsManager.hasValid(minTTL: minTTL) +} +``` + +### Force Token Renewal + +```swift +do { + let credentials = try await credentialsManager.renew() + // Renewed token available at credentials.accessToken +} catch { + print("Renewal failed: \(error)") +} +``` + +### Revoke Refresh Token + +```swift +// Revokes the refresh token on Auth0 and clears local credentials +try await credentialsManager.revoke() +``` + +--- + +## Biometric Protection + +Protect credential retrieval with Face ID / Touch ID. + +> **Physical device note:** Biometric authentication (Face ID / Touch ID) requires a real device. The iOS Simulator supports simulated biometrics but physical device testing is required before shipping to verify actual hardware behavior. + +### Enable Biometrics + +```swift +let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) + +// Basic — system-managed prompt reuse +credentialsManager.enableBiometrics(withTitle: "Authenticate to access your account") + +// With session timeout (reuse for 5 minutes) +credentialsManager.enableBiometrics( + withTitle: "Authenticate to access your account", + policy: .session(timeoutInSeconds: 300) +) + +// Require fresh biometric every time +credentialsManager.enableBiometrics( + withTitle: "Authenticate to access your account", + policy: .always +) + +// App lifecycle (reset on app background/foreground) +credentialsManager.enableBiometrics( + withTitle: "Authenticate to access your account", + policy: .appLifecycle(timeoutInSeconds: 3600) +) +``` + +### Handle Biometric Errors + +```swift +do { + let credentials = try await credentialsManager.credentials() + useCredentials(credentials) +} catch CredentialsManagerError.biometricsFailed { + // Biometric auth failed — ask user to log in again + _ = credentialsManager.clear() + await login() +} catch CredentialsManagerError.noCredentialsAvailable { + await login() +} +``` + +### Info.plist Permission (Required) + +Add to your app's `Info.plist`: +```xml +NSFaceIDUsageDescription +Authenticate to access your account securely. +``` + +--- + +## Error Handling + +### Web Auth Errors + +```swift +do { + let credentials = try await Auth0.webAuth().start() +} catch WebAuthError.userCancelled { + // User tapped Cancel — no action needed, just return to UI +} catch WebAuthError.noCredentialsAvailable { + print("No credentials available — unexpected after login") +} catch WebAuthError.pkceNotAllowed { + print("PKCE not enabled — check Auth0 Dashboard → Application → Advanced Settings → OAuth") +} catch { + // Other error (network, configuration) + print("Web Auth error: \(error)") +} +``` + +### CredentialsManager Errors + +```swift +do { + let credentials = try await credentialsManager.credentials() +} catch CredentialsManagerError.noCredentialsAvailable { + // First launch or after logout + await showLoginScreen() +} catch CredentialsManagerError.failedToRenewCredentials(let renewalError) { + // Refresh token expired — must re-authenticate + _ = credentialsManager.clear() + await showLoginScreen() +} catch CredentialsManagerError.biometricsFailed { + // Face ID / Touch ID failed + await showBiometricFailureMessage() +} catch CredentialsManagerError.cannotAccessKeychainItem { + // Keychain access denied (e.g., device locked, missing entitlements) + print("Keychain error: \(error)") +} +``` + +### Authentication API Errors + +```swift +Auth0 + .authentication() + .login(usernameOrEmail: "user@example.com", + password: "password", + realmOrConnection: "Username-Password-Authentication", + scope: "openid profile email offline_access") + .start { result in + switch result { + case .success(let credentials): + // Access token available at credentials.accessToken + case .failure(let error) where error.isMultifactorRequired: + // Extract MFA token for MFA challenge flow + if let mfaPayload = error.mfaRequiredErrorPayload { + startMFAChallenge(mfaToken: mfaPayload.mfaToken) + } + case .failure(let error) where error.isNetworkError: + showNetworkError() + case .failure(let error): + print("Auth error code: \(error.code), description: \(error.localizedDescription)") + } + } +``` + +--- + +## MFA (Multi-Factor Authentication) + +### Handling MFA Required Error + +```swift +// When login returns isMultifactorRequired = true, challenge with OTP +func verifyMFA(mfaToken: String, otp: String) async throws -> Credentials { + return try await Auth0 + .authentication() + .multifactorChallenge(mfaToken: mfaToken, types: ["otp"]) + .start() +} +``` + +--- + +## Organizations + +### Login to a Specific Organization + +```swift +try await Auth0 + .webAuth() + .useHTTPS() + .organization("YOUR_ORG_ID") + .start() +``` + +### Accept Organization Invitation + +```swift +// Handle invitation URL from deep link +func handleInvitation(url: URL) async { + try? await Auth0 + .webAuth() + .useHTTPS() + .invitationURL(url) + .start() +} +``` + +--- + +## Platform-Specific Patterns + +### SwiftUI App Lifecycle (Recommended) + +```swift +// MyApp.swift +import SwiftUI +import Auth0 + +@main +struct MyApp: App { + @StateObject private var auth = AuthenticationService() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(auth) + } + } +} + +// ContentView.swift +struct ContentView: View { + @EnvironmentObject var auth: AuthenticationService + + var body: some View { + Group { + if auth.isAuthenticated { + HomeView() + } else { + LoginView() + } + } + .onAppear { + auth.checkSession() + } + } +} +``` + +### UIKit App Lifecycle + +```swift +// AppDelegate.swift +import UIKit +import Auth0 + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Required for SFSafariViewController or custom URL scheme + return WebAuthentication.resume(with: url) + } +} + +// SceneDelegate.swift (if using scenes) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + WebAuthentication.resume(with: url) + } +} +``` + +### Using SFSafariViewController (Instead of ASWebAuthenticationSession) + +```swift +// For apps that cannot use ASWebAuthenticationSession +Auth0 + .webAuth() + .provider(WebAuthentication.safariProvider()) + .start { result in + switch result { + case .success(let credentials): + print("Login success") + case .failure(let error): + print("Login failed: \(error)") + } + } +``` + +> **Note:** SFSafariViewController requires `WebAuthentication.resume(with:)` to be called from `AppDelegate` or `SceneDelegate` (see UIKit pattern above). + +--- + +## App Groups (Shared Keychain Access) + +To share credentials between your app and extensions (widgets, share extensions): + +```swift +// Use a custom storeKey to write to a shared Keychain group +let credentialsManager = CredentialsManager( + authentication: Auth0.authentication(), + storeKey: "com.yourcompany.sharedCredentials" +) + +// Configure Keychain sharing in Xcode: +// Target → Signing & Capabilities → + Capability → Keychain Sharing +// Add a shared Keychain group name +``` + +--- + +## Calling Your API with the Access Token + +```swift +func fetchData() async throws -> [Item] { + let credentials = try await credentialsManager.credentials() + + var request = URLRequest(url: URL(string: "https://your-api.example.com/items")!) + request.setValue("Bearer \(credentials.accessToken)", forHTTPHeaderField: "Authorization") + + let (data, _) = try await URLSession.shared.data(for: request) + return try JSONDecoder().decode([Item].self, from: data) +} +``` diff --git a/main/.mintlify/skills/auth0-swift/references/setup.md b/main/.mintlify/skills/auth0-swift/references/setup.md new file mode 100644 index 0000000000..de0dbe4f80 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/references/setup.md @@ -0,0 +1,236 @@ +# Setup Guide — Auth0 Swift + +## Auth0 Configuration + +> **Agent instruction:** Check whether Auth0 credentials (domain and client ID) are already provided in the user's prompt. +> - **If credentials are provided:** Write `Auth0.plist` directly with those values and proceed to "Post-Setup Steps". +> - **If no credentials are provided:** Run the bootstrap script below. Do NOT offer manual setup or ask the user to create an application in Auth0 Dashboard themselves — always use the CLI. + +### Bootstrap Script + +> **Agent instruction:** Run these checks before the script. Do NOT run `auth0 login` from the agent — it is interactive and will hang. +> +> 1. **Check Node.js**: `node --version`. If missing or below 20, install it: `brew install node`. +> 2. **Check Auth0 CLI**: `command -v auth0`. If missing, install it: `brew install auth0/auth0-cli/auth0`. +> 3. **Check Auth0 login**: `auth0 tenants list --csv --no-input 2>&1`. If it fails or returns empty: +> - Tell the user: _"Please run `auth0 login` in your terminal and let me know when done."_ +> - Wait for confirmation, then re-run the check. Retry up to 3 times before treating as a persistent failure. +> 4. **Confirm active tenant**: Parse the `→` line from the CSV output. Tell the user: _"Your active Auth0 tenant is: ``. Is this correct?"_ +> - If no, ask the user to run `auth0 tenants use `, then re-run step 3. +> +> Once confirmed, run: +> ```bash +> cd /auth0-swift/scripts +> npm install +> node bootstrap.mjs +> ``` +> +> If the script fails due to session expiry, ask the user to run `auth0 login` again, then re-run. Retry up to 3 times. +> Only if the script keeps failing after retries: use `AskUserQuestion` to ask the user for their Auth0 Domain and Client ID, then write `Auth0.plist` with those values. + +The script will: +1. Detect your bundle identifier from `project.pbxproj` (`PRODUCT_BUNDLE_IDENTIFIER`) +2. Create a **Native** application in Auth0 Dashboard +3. Register both `https://` and `{bundle}://` callback + logout URLs +4. Set up a database connection (Username-Password-Authentication) +5. Write `Auth0.plist` to your project directory + +### Writing Auth0.plist (credentials already known) + +Use this only when credentials are explicitly provided by the user or obtained after bootstrap script failure. + +```xml + + + + + ClientId + YOUR_AUTH0_CLIENT_ID + Domain + YOUR_AUTH0_DOMAIN + + +``` + +Add the file to the Xcode project: +1. Right-click on the project in Navigator → **Add Files to "YourProject"** +2. Select `Auth0.plist` +3. Ensure your app target is checked in the "Add to targets" list + +--- + +## Post-Setup Steps + +### Register URL Scheme (Required for Custom Scheme Callbacks) + +In Xcode, select your app target → **Info** tab → expand **URL Types** → click **+**: +- **Identifier**: `auth0` +- **URL Schemes**: `$(PRODUCT_BUNDLE_IDENTIFIER)` + +This allows the Auth0 browser to redirect back to your app using the `{bundle}://` scheme. + +### Associated Domains Setup (HTTPS Universal Links) + +> **Agent instruction:** Only follow this section if the user chose HTTPS Universal Links as their callback scheme. If they chose a custom scheme (`{bundle}://`), skip this section entirely. +> +> **Prerequisites:** Before configuring Xcode, Auth0 must be told your Apple Team ID and Bundle ID so it can host the `apple-app-site-association` file. Without this, Universal Links will not work even if the entitlements are correct. + +#### Step 1 — Configure Device Settings in Auth0 Dashboard + +> **Agent instruction:** +> 1. In Auth0 Dashboard → **Applications** → your app → **Settings**, scroll to the bottom and click **Show Advanced Settings** +> 2. Select the **Device Settings** tab +> 3. Enter the **Apple Team ID** — found at [developer.apple.com/account](https://developer.apple.com/account) under Membership Details +> 4. Enter the **App Bundle Identifier** (e.g. `com.example.myapp`) +> 5. Click **Save Changes** +> +> Auth0 will now automatically host the Apple App Site Association file at: +> `https://YOUR_AUTH0_DOMAIN/.well-known/apple-app-site-association` +> +> Verify it lists your app by opening that URL — the response should contain `applinks` with your `appID` in the format `TEAMID.com.example.myapp`. +> +> Reference: [Enable Universal Links Support in Apple Xcode](https://auth0.com/docs/get-started/applications/enable-universal-links-support-in-apple-xcode) + +#### Step 2 — Add Associated Domains Entitlement in Xcode + +> **Agent instruction:** +> 1. Find the app's `.entitlements` file (commonly `.entitlements`). Search for `*.entitlements` in the project directory. +> 2. If the file exists, add `com.apple.developer.associated-domains` to it. If it does not exist, create it at the project root alongside the `.xcodeproj`. +> 3. Add both entries using the actual Auth0 domain: + +```xml +com.apple.developer.associated-domains + + applinks:YOUR_AUTH0_DOMAIN + webcredentials:YOUR_AUTH0_DOMAIN + +``` + +> - `applinks:` — routes the Universal Link callback back to your app after login +> - `webcredentials:` — enables Password AutoFill and credential handoff with Auth0 +> +> 4. If `com.apple.developer.associated-domains` already exists in the file, append the two `` entries to the existing array rather than replacing it. +> 5. If the file was newly created, check that `CODE_SIGN_ENTITLEMENTS` in the target's build settings points to it. If not, inform the user to set it in Xcode under target → Build Settings → Code Signing Entitlements. +> 6. Ensure `.useHTTPS()` is called on the `webAuth()` builder: +> ```swift +> Auth0.webAuth().useHTTPS() +> ``` + +### Verify Auth0.plist Target Membership + +In Xcode Project Navigator: +1. Click `Auth0.plist` +2. Open File Inspector (right panel, first tab) +3. Under **Target Membership**, ensure your app target checkbox is checked + +### macOS Additional Steps + +For macOS targets, also: +1. Select your app target → **Signing & Capabilities** tab +2. Click **+ Capability** → add **Outgoing Connections (Client)** +3. Register macOS callback URLs in Auth0 Dashboard: + ```text + https://YOUR_DOMAIN/macos/YOUR_BUNDLE_ID/callback, + YOUR_BUNDLE_ID://YOUR_DOMAIN/macos/YOUR_BUNDLE_ID/callback + ``` + +--- + +## SDK Installation + +> **Agent instruction:** Before proceeding, check the project directory for signs of an existing package manager: +> - `Podfile` present → use **CocoaPods** +> - `Cartfile` present → use **Carthage** +> - `Package.swift` present → use **Swift Package Manager** +> +> If none are found, ask the user via `AskUserQuestion`: _"Which dependency manager does your project use — Swift Package Manager, CocoaPods, or Carthage?"_ Then follow only the matching section below. + +### Swift Package Manager (Recommended) + +#### Package.swift project + +Run in the project root: + +```bash +swift package add-dependency https://github.com/auth0/Auth0.swift --from 2.18.0 +``` + +Then add `"Auth0"` to the target's `dependencies` array in `Package.swift`: + +```swift +.target( + name: "YourTarget", + dependencies: ["Auth0"] +) +``` + +#### Xcode project (`.xcodeproj`, no `Package.swift`) + +The `swift package add-dependency` command does not apply to Xcode projects. Add the package via the Xcode GUI: + +1. **File → Add Package Dependencies** +2. Enter package URL: `https://github.com/auth0/Auth0.swift` +3. Select **Up to Next Major Version** starting from `2.18.0` +4. Click **Add Package** +5. In the package product list, ensure **Auth0** is added to your app target + +### CocoaPods + +```ruby +# Podfile +target 'YourApp' do + use_frameworks! + pod 'Auth0', '~> 2.18' +end +``` + +```bash +pod install +# IMPORTANT: Always open .xcworkspace after pod install +open YourApp.xcworkspace +``` + +### Carthage + +```text +# Cartfile +github "auth0/Auth0.swift" ~> 2.18 +``` + +```bash +# Build frameworks +carthage update --use-xcframeworks --platform iOS + +# Then in Xcode: Target → General → "Frameworks, Libraries, and Embedded Content" +# Drag in Carthage/Build/iOS/Auth0.xcframework +``` + +--- + +## Secret Management + +Auth0.swift **does not use a client secret**. Native apps use PKCE (Proof Key for Code Exchange), which is secure without a secret. + +- `ClientId` and `Domain` in `Auth0.plist` are **not secrets** — they are safe to commit to source control +- Access tokens and refresh tokens are stored in the iOS/macOS **Keychain** by `CredentialsManager` — never in `UserDefaults` or plain files +- No environment variables or `.env` files are needed for the Auth0 configuration + +--- + +## Verification + +After completing setup, verify: + +```bash +# 1. Build the project +xcodebuild build -scheme YOUR_SCHEME -destination "platform=iOS Simulator,name=iPhone 16" + +# 2. Verify Auth0.plist is bundled (should print your domain) +# Run app in Simulator and check Xcode console for Auth0 initialization +``` + +- [ ] `Auth0.plist` is in the project and in the app target +- [ ] URL scheme `$(PRODUCT_BUNDLE_IDENTIFIER)` is registered in Info tab +- [ ] Callback URLs are saved in Auth0 Dashboard +- [ ] App builds without errors +- [ ] `import Auth0` resolves without errors in Swift files diff --git a/main/.mintlify/skills/auth0-swift/scripts/bootstrap.mjs b/main/.mintlify/skills/auth0-swift/scripts/bootstrap.mjs new file mode 100644 index 0000000000..a7630deea3 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/bootstrap.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +import path from "node:path" + +import { + checkNodeVersion, + checkAuth0CLI, + getActiveTenant, + validateSwiftProject, +} from "./utils/validation.mjs" +import { + discoverExistingConnections, + buildChangePlan, + displayChangePlan, +} from "./utils/discovery.mjs" +import { + applyNativeClientChanges, + applyDeviceSettings, +} from "./utils/clients.mjs" +import { applyDatabaseConnectionChanges, checkDatabaseConnectionChanges } from "./utils/connections.mjs" +import { writeAuth0Plist } from "./utils/plist-writer.mjs" +import { writeEntitlementsFile } from "./utils/entitlements.mjs" +import { configureXcodeProject } from "./utils/xcode-project.mjs" +import { confirmWithUser } from "./utils/helpers.mjs" + +async function main() { + console.log("\n Auth0 Swift Bootstrap\n") + + const projectPath = path.resolve(process.argv[2] || process.cwd()) + + // Pre-flight checks + checkNodeVersion() + await checkAuth0CLI() + const domain = await getActiveTenant() + + // Validate Xcode project and detect bundle identifier + team ID + const config = validateSwiftProject(projectPath) + + // Discover existing connections + build change plan + const connections = await discoverExistingConnections() + const plan = buildChangePlan(connections, domain, config) + displayChangePlan(plan) + + // Confirm with user + const confirmed = await confirmWithUser("Apply these changes?") + if (!confirmed) { + console.log("\n Aborted by user.\n") + process.exit(0) + } + + console.log("") + + // 1. Create Native app (registers HTTPS + custom scheme callback URLs) + const client = await applyNativeClientChanges(plan.client) + + // 2. Set up database connection + plan.connection = checkDatabaseConnectionChanges(connections, client.client_id) + await applyDatabaseConnectionChanges(plan.connection, client.client_id) + + // 3. Configure Device Settings so Auth0 hosts apple-app-site-association + await applyDeviceSettings(client.client_id, config.teamId, config.bundleId) + + // 4. Write Auth0.plist + await writeAuth0Plist(domain, client.client_id, config.auth0PlistPath) + + // 5. Write / merge entitlements file with Associated Domains entries + writeEntitlementsFile(config.entitlementsPath, domain) + + // 6. Add Auth0.plist to Xcode target + set CODE_SIGN_ENTITLEMENTS + const xcodeConfigured = await configureXcodeProject( + config.xcodeprojPath, + config.auth0PlistPath, + path.basename(config.entitlementsPath) + ) + + // Summary + console.log("\n Auth0 Swift Setup Complete\n") + console.log(` Domain: ${domain}`) + console.log(` Client ID: ${client.client_id}`) + console.log(` Bundle ID: ${config.bundleId}`) + console.log(` Auth0.plist: ${config.auth0PlistPath}`) + console.log(` Entitlements: ${config.entitlementsPath}`) + console.log("") + + const remainingSteps = [] + + remainingSteps.push( + "1. Register URL scheme in Xcode: target → Info tab → URL Types → +\n" + + " Identifier: auth0 | URL Schemes: $(PRODUCT_BUNDLE_IDENTIFIER)" + ) + + if (!config.teamId) { + remainingSteps.push( + "2. Apple Team ID was not detected — verify Device Settings in Auth0 Dashboard:\n" + + " App Settings → Advanced → Device Settings → Team ID + Bundle ID" + ) + } + + if (!xcodeConfigured) { + remainingSteps.push( + "3. xcodeproj gem unavailable — complete manually in Xcode:\n" + + ` a. Add Auth0.plist to target: right-click → Add Files → check ${config.appName}\n` + + ` b. Set CODE_SIGN_ENTITLEMENTS = "${path.basename(config.entitlementsPath)}" in target Build Settings` + ) + } + + if (remainingSteps.length > 0) { + console.log(" Remaining manual steps:") + for (const step of remainingSteps) { + console.log(` ${step}`) + } + console.log("") + } +} + +main().catch((e) => { + console.error(`\n Bootstrap failed: ${e.message}\n`) + process.exit(1) +}) diff --git a/main/.mintlify/skills/auth0-swift/scripts/package.json b/main/.mintlify/skills/auth0-swift/scripts/package.json new file mode 100644 index 0000000000..d2cc1206d0 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "auth0-swift-bootstrap", + "version": "1.0.0", + "description": "Bootstrap Auth0 configuration for iOS/macOS Swift projects", + "type": "module", + "scripts": { + "auth0:bootstrap": "node bootstrap.mjs" + }, + "dependencies": { + "@inquirer/prompts": "^8.1.0", + "execa": "^9.0.0", + "ora": "^8.0.0" + } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/auth0-api.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/auth0-api.mjs new file mode 100644 index 0000000000..7784d93494 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/auth0-api.mjs @@ -0,0 +1,24 @@ +import { $ } from "execa" + +/** + * Make a generic API call using auth0 CLI. + */ +export async function auth0ApiCall(method, endpoint, data = null) { + const args = ["api", method, endpoint, "--no-input"] + + if (data) { + args.push("--data", JSON.stringify(data)) + } + + try { + const { stdout } = await $({ timeout: 30000 })`auth0 ${args}` + return stdout ? JSON.parse(stdout) : null + } catch (e) { + if (e.timedOut) { + console.warn(`Warning: API call timed out: auth0 api ${method} ${endpoint}`) + } else { + console.warn(`Warning: API call failed: ${e.message}`) + } + throw e + } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/change-plan.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/change-plan.mjs new file mode 100644 index 0000000000..31a747c108 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/change-plan.mjs @@ -0,0 +1,12 @@ +export const ChangeAction = { + CREATE: "create", + UPDATE: "update", + SKIP: "skip", +} + +export function createChangeItem(action, details = {}) { + return { + action, + ...details, + } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/clients.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/clients.mjs new file mode 100644 index 0000000000..a495df1e2a --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/clients.mjs @@ -0,0 +1,84 @@ +import { $ } from "execa" +import ora from "ora" +import { ChangeAction, createChangeItem } from "./change-plan.mjs" +import { auth0ApiCall } from "./auth0-api.mjs" +import { getInputFromUser } from "./helpers.mjs" + +/** + * Build the change plan item for a Native Auth0 application (iOS/macOS). + * Registers both HTTPS (Universal Links) and custom scheme callback URLs. + */ +export function checkNativeClientChanges(domain, swiftConfig) { + const { bundleId } = swiftConfig + const httpsCallback = `https://${domain}/ios/${bundleId}/callback` + const customCallback = `${bundleId}://${domain}/ios/${bundleId}/callback` + + return createChangeItem(ChangeAction.CREATE, { + resource: "Native Client", + name: `${bundleId}-ios`, + httpsCallback, + customCallback, + }) +} + +/** + * Create a Native application in Auth0 via the CLI. + * Registers both HTTPS and custom scheme callback + logout URLs. + * Returns the created client object (includes client_id). + */ +export async function applyNativeClientChanges(changePlan) { + if (changePlan.action !== ChangeAction.CREATE) { + return { client_id: changePlan.clientId } + } + + const spinner = ora(`Creating Native Client: ${changePlan.name}`).start() + try { + const callbacks = `${changePlan.httpsCallback},${changePlan.customCallback}` + const createArgs = [ + "apps", "create", + "--name", changePlan.name, + "--type", "native", + "--auth-method", "none", + "--callbacks", callbacks, + "--logout-urls", callbacks, + "--json", + "--no-input", + ] + const { stdout } = await $({ timeout: 30000 })`auth0 ${createArgs}` + const client = JSON.parse(stdout) + spinner.succeed(`Created Native Client: ${changePlan.name} (${client.client_id})`) + return client + } catch (e) { + spinner.fail("Failed to create Native Client") + throw e + } +} + +/** + * Set iOS Device Settings (Team ID + Bundle ID) on the Auth0 application. + * Required for Auth0 to host the apple-app-site-association file for Universal Links. + * If teamId is not available from the project, prompts the user. + */ +export async function applyDeviceSettings(clientId, teamId, bundleId) { + const resolvedTeamId = teamId || await getInputFromUser( + " Apple Team ID not found in project. Enter it (developer.apple.com → Account → Membership, 10 characters):" + ) + + if (!resolvedTeamId) { + const spinner = ora("Skipping Device Settings").start() + spinner.warn("No Team ID provided — set it manually in Auth0 Dashboard → App Settings → Advanced → Device Settings") + return null + } + + const spinner = ora("Configuring Universal Links device settings").start() + try { + await auth0ApiCall("patch", `applications/${clientId}`, { + mobile: { ios: { team_id: resolvedTeamId, app_bundle_identifier: bundleId } }, + }) + spinner.succeed(`Configured Device Settings: Team ID ${resolvedTeamId}, Bundle ${bundleId}`) + return resolvedTeamId + } catch (e) { + spinner.fail("Failed to configure Device Settings") + throw e + } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/connections.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/connections.mjs new file mode 100644 index 0000000000..90a2a0d34e --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/connections.mjs @@ -0,0 +1,77 @@ +import ora from "ora" + +import { auth0ApiCall } from "./auth0-api.mjs" +import { ChangeAction, createChangeItem } from "./change-plan.mjs" + +export const DEFAULT_CONNECTION_NAME = "Username-Password-Authentication" + +export function checkDatabaseConnectionChanges(existingConnections, clientId) { + const existing = existingConnections.find((c) => c.name === DEFAULT_CONNECTION_NAME) + + if (!existing) { + return createChangeItem(ChangeAction.CREATE, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + enabledClients: [clientId], + }) + } + + const enabledClients = existing.enabled_clients || [] + if (!enabledClients.includes(clientId)) { + return createChangeItem(ChangeAction.UPDATE, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + existing, + summary: "Enable client on connection", + }) + } + + return createChangeItem(ChangeAction.SKIP, { + resource: "Database Connection", + name: DEFAULT_CONNECTION_NAME, + existing, + }) +} + +export async function applyDatabaseConnectionChanges(changePlan, clientId) { + if (changePlan.action === ChangeAction.SKIP) { + const spinner = ora(`Database Connection is up to date: ${changePlan.name}`).start() + spinner.succeed() + return changePlan.existing + } + + if (changePlan.action === ChangeAction.CREATE) { + const spinner = ora(`Creating Database Connection: ${DEFAULT_CONNECTION_NAME}`).start() + try { + const connectionData = { + strategy: "auth0", + name: DEFAULT_CONNECTION_NAME, + enabled_clients: [clientId], + } + const connection = await auth0ApiCall("post", "connections", connectionData) + spinner.succeed(`Created Database Connection: ${DEFAULT_CONNECTION_NAME}`) + return connection + } catch (e) { + spinner.fail("Failed to create Database Connection") + throw e + } + } + + if (changePlan.action === ChangeAction.UPDATE) { + const spinner = ora(`Updating Database Connection: ${DEFAULT_CONNECTION_NAME}`).start() + try { + const existing = changePlan.existing + const updatedClients = [...(existing.enabled_clients || []), clientId] + await auth0ApiCall("patch", `connections/${existing.id}`, { + enabled_clients: updatedClients, + }) + spinner.succeed(`Updated ${DEFAULT_CONNECTION_NAME}: enabled client ${clientId}`) + return { ...existing, enabled_clients: updatedClients } + } catch (e) { + spinner.fail("Failed to update Database Connection") + throw e + } + } + + throw new Error(`Unknown change action: ${changePlan.action}`) +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/discovery.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/discovery.mjs new file mode 100644 index 0000000000..84910177f7 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/discovery.mjs @@ -0,0 +1,66 @@ +import ora from "ora" +import { auth0ApiCall } from "./auth0-api.mjs" +import { ChangeAction } from "./change-plan.mjs" +import { checkNativeClientChanges } from "./clients.mjs" +import { checkDatabaseConnectionChanges } from "./connections.mjs" + +/** + * Fetch existing connections from the Auth0 tenant. + */ +export async function discoverExistingConnections() { + const spinner = ora("Discovering existing connections").start() + try { + const connections = (await auth0ApiCall("get", "connections")) || [] + spinner.succeed("Discovered existing connections") + return connections + } catch (e) { + const msg = e.message || String(e) + if (msg.includes("404") || msg.includes("Not Found")) { + spinner.succeed("No existing connections found") + return [] + } + spinner.fail("Failed to discover connections") + throw e + } +} + +/** + * Build a change plan for a Swift (iOS/macOS) project. + * Creates plan items for: Native Client + Database Connection. + */ +export function buildChangePlan(connections, domain, platformConfig) { + const clientPlan = checkNativeClientChanges(domain, platformConfig) + // Connection will be linked after client is created; use placeholder for now + const connectionPlan = checkDatabaseConnectionChanges(connections, "TO_BE_CREATED") + return { client: clientPlan, connection: connectionPlan } +} + +/** + * Print the change plan to the console for user review. + */ +export function displayChangePlan(plan) { + console.log("\n Change Plan:\n") + + const items = [ + { name: "Native Client", ...plan.client }, + { name: "Database Connection", ...plan.connection }, + ] + + for (const item of items) { + const icon = + item.action === ChangeAction.CREATE ? "+" : + item.action === ChangeAction.UPDATE ? "~" : "=" + const label = + item.action === ChangeAction.CREATE ? "CREATE" : + item.action === ChangeAction.UPDATE ? "UPDATE" : "SKIP " + + let detail = "" + if (item.summary) detail = ` (${item.summary})` + else if (item.httpsCallback) detail = ` (callbacks: ${item.httpsCallback}, ${item.customCallback})` + else if (item.callbackUrl) detail = ` (callback: ${item.callbackUrl})` + + console.log(` ${icon} [${label}] ${item.name || item.resource}${detail}`) + } + + console.log("") +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/entitlements.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/entitlements.mjs new file mode 100644 index 0000000000..5719e4854d --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/entitlements.mjs @@ -0,0 +1,71 @@ +import fs from "node:fs" +import ora from "ora" + +/** + * Write or merge the Associated Domains entitlements file. + * + * - If the file does not exist: creates it with applinks + webcredentials entries. + * - If the file exists but lacks the entries: merges them into the existing content. + * - If the file already contains both entries: no-op. + */ +export function writeEntitlementsFile(entitlementsPath, domain) { + const applinksEntry = `applinks:${domain}` + const webcredEntry = `webcredentials:${domain}` + const spinner = ora(`Writing entitlements: ${entitlementsPath.split("/").pop()}`).start() + + try { + if (!fs.existsSync(entitlementsPath)) { + fs.writeFileSync(entitlementsPath, buildEntitlementsContent(domain), "utf-8") + spinner.succeed(`Created entitlements file with Associated Domains`) + return + } + + let content = fs.readFileSync(entitlementsPath, "utf-8") + + if (content.includes(applinksEntry) && content.includes(webcredEntry)) { + spinner.succeed(`Entitlements already contain Associated Domains entries`) + return + } + + if (content.includes("com.apple.developer.associated-domains")) { + // Key exists — append missing entries into its array + const arrayPattern = /(com\.apple\.developer\.associated-domains<\/key>\s*)([\s\S]*?)(<\/array>)/ + content = content.replace(arrayPattern, (_, open, body, close) => { + if (!body.includes(`applinks:${domain}`)) body += `\n ${applinksEntry}` + if (!body.includes(`webcredentials:${domain}`)) body += `\n ${webcredEntry}` + return `${open}${body}${close}` + }) + } else { + // No associated-domains key — add it before + const newSection = + ` com.apple.developer.associated-domains\n` + + ` \n` + + ` ${applinksEntry}\n` + + ` ${webcredEntry}\n` + + ` \n` + content = content.replace("", `${newSection}`) + } + + fs.writeFileSync(entitlementsPath, content, "utf-8") + spinner.succeed(`Updated entitlements with Associated Domains`) + } catch (e) { + spinner.fail("Failed to write entitlements file") + throw e + } +} + +function buildEntitlementsContent(domain) { + return ( + `\n` + + `\n` + + `\n` + + `\n` + + ` com.apple.developer.associated-domains\n` + + ` \n` + + ` applinks:${domain}\n` + + ` webcredentials:${domain}\n` + + ` \n` + + `\n` + + `\n` + ) +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/helpers.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/helpers.mjs new file mode 100644 index 0000000000..308b4688ae --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/helpers.mjs @@ -0,0 +1,31 @@ +import readline from "node:readline/promises" +import { select } from "@inquirer/prompts" + +export async function confirmWithUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} (y/N): `) + rl.close() + + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes" +} + +export async function getInputFromUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} `) + rl.close() + + return answer.trim() +} + +export async function selectOptionFromList(message, options) { + const answer = await select({ message, choices: options }) + return answer +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/plist-writer.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/plist-writer.mjs new file mode 100644 index 0000000000..20ac38a9b6 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/plist-writer.mjs @@ -0,0 +1,30 @@ +import fs from "node:fs" +import ora from "ora" + +/** + * Write Auth0.plist with ClientId and Domain. + * Overwrites the file if it already exists. + */ +export async function writeAuth0Plist(domain, clientId, auth0PlistPath) { + const spinner = ora("Writing Auth0.plist").start() + + try { + const plist = + '\n' + + '\n' + + '\n' + + '\n' + + ' ClientId\n' + + ` ${clientId}\n` + + ' Domain\n' + + ` ${domain}\n` + + '\n' + + '\n' + + fs.writeFileSync(auth0PlistPath, plist, "utf-8") + spinner.succeed(`Wrote ${auth0PlistPath}`) + } catch (e) { + spinner.fail("Failed to write Auth0.plist") + throw e + } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/validation.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/validation.mjs new file mode 100644 index 0000000000..a78db77986 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/validation.mjs @@ -0,0 +1,118 @@ +import { $ } from "execa" +import fs from "node:fs" +import path from "node:path" +import ora from "ora" + +// --------------------------------------------------------------------------- +// Shared preflight — identical for all SDK types +// --------------------------------------------------------------------------- + +export function checkNodeVersion() { + const [major] = process.versions.node.split(".").map(Number) + if (major < 20) { + console.error(`Node.js 20 or later is required (current: ${process.version})`) + process.exit(1) + } +} + +export async function checkAuth0CLI() { + const spinner = ora("Checking Auth0 CLI").start() + try { + const versionArgs = ["--version", "--no-input"] + const { stdout } = await $({ timeout: 10000 })`auth0 ${versionArgs}` + spinner.succeed(`Auth0 CLI found: ${stdout.trim()}`) + } catch { + spinner.fail("Auth0 CLI is not installed") + console.error( + "\nInstall it:\n" + + " macOS: brew install auth0/auth0-cli/auth0\n" + + " Linux: curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh\n" + + " More: https://github.com/auth0/auth0-cli\n" + ) + process.exit(1) + } +} + +export async function getActiveTenant() { + const spinner = ora("Detecting active tenant").start() + try { + const tenantsArgs = ["tenants", "list", "--csv", "--no-input"] + const { stdout } = await $({ timeout: 10000 })`auth0 ${tenantsArgs}` + + const activeLine = stdout + .split("\n") + .slice(1) + .find((line) => line.includes("\u2192")) + + const domain = activeLine?.split(",")[1]?.trim() + if (!domain) { + spinner.fail("No active tenant. Run `auth0 login` then re-run this script.") + process.exit(1) + } + + spinner.succeed(`Active tenant: ${domain}`) + return domain + } catch { + spinner.fail("Not logged in. Run `auth0 login` then re-run this script.") + process.exit(1) + } +} + +// --------------------------------------------------------------------------- +// Swift / iOS / macOS project validator +// --------------------------------------------------------------------------- + +export function validateSwiftProject(projectPath) { + const spinner = ora("Validating Swift project").start() + + const entries = fs.readdirSync(projectPath) + const xcodeproj = entries.find((e) => e.endsWith(".xcodeproj")) + const xcworkspace = entries.find((e) => e.endsWith(".xcworkspace")) + + if (!xcodeproj && !xcworkspace) { + spinner.fail(`No .xcodeproj or .xcworkspace found in ${projectPath}`) + console.error("\n Ensure you're pointing to the directory containing your Xcode project.\n") + process.exit(1) + } + + let bundleId = null + + if (xcodeproj) { + const pbxprojPath = path.join(projectPath, xcodeproj, "project.pbxproj") + if (fs.existsSync(pbxprojPath)) { + const content = fs.readFileSync(pbxprojPath, "utf-8") + const regex = /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g + let match + while ((match = regex.exec(content)) !== null) { + const value = match[1].trim().replace(/"/g, "") + // Skip variables, test targets, and invalid values + if (value.includes("$(") || value.includes("Tests") || value === "NO") { + continue + } + bundleId = value + break + } + } + } + + if (!bundleId) { + spinner.fail("Could not detect Bundle Identifier from Xcode project") + console.error( + "\n Parsed: " + (xcodeproj ? xcodeproj + "/project.pbxproj" : "no .xcodeproj found") + + "\n Please provide your Bundle Identifier manually.\n" + ) + process.exit(1) + } + + // Extract Team ID (DEVELOPMENT_TEAM) — may be absent if project is not yet signed + const teamIdMatch = content.match(/DEVELOPMENT_TEAM\s*=\s*([A-Z0-9]{10})\s*;/) + const teamId = teamIdMatch?.[1] || null + + const appName = xcodeproj.replace(".xcodeproj", "") + const xcodeprojPath = path.join(projectPath, xcodeproj) + const auth0PlistPath = path.join(projectPath, "Auth0.plist") + const entitlementsPath = path.join(projectPath, `${appName}.entitlements`) + + spinner.succeed(`Swift project: ${bundleId} (${xcodeproj || xcworkspace})`) + return { bundleId, teamId, appName, xcodeprojPath, auth0PlistPath, entitlementsPath } +} diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-modify.rb b/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-modify.rb new file mode 100644 index 0000000000..c8bd5a18b5 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-modify.rb @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# xcode-modify.rb +# Modifies an Xcode project using the xcodeproj gem: +# 1. Adds a file (Auth0.plist) to the main app target's Resources build phase. +# 2. Sets CODE_SIGN_ENTITLEMENTS on all target build configurations. +# +# Usage: ruby xcode-modify.rb + +require 'pathname' + +begin + require 'xcodeproj' +rescue LoadError + STDERR.puts "xcodeproj gem not found — install with: gem install xcodeproj" + exit 2 +end + +xcodeproj_path, plist_abs_path, entitlements_rel_path = ARGV + +project = Xcodeproj::Project.open(xcodeproj_path) +project_dir = Pathname.new(File.dirname(xcodeproj_path)) + +# Find the main application target (prefer .app product type, skip test targets) +target = project.native_targets.find { |t| t.product_type == "com.apple.product-type.application" } +target ||= project.native_targets.reject { |t| t.name.match?(/[Tt]est/) }.first +target ||= project.native_targets.first + +if target.nil? + STDERR.puts "No suitable target found in #{xcodeproj_path}" + exit 1 +end + +# 1. Add Auth0.plist to target's Resources build phase +if plist_abs_path && !plist_abs_path.empty? + plist_name = File.basename(plist_abs_path) + plist_rel = Pathname.new(plist_abs_path).relative_path_from(project_dir).to_s + + existing_ref = project.files.find { |f| File.basename(f.path.to_s) == plist_name } + + unless existing_ref + existing_ref = project.main_group.new_file(plist_rel) + existing_ref.source_tree = "SOURCE_ROOT" + existing_ref.last_known_file_type = "text.plist.xml" + end + + in_resources = target.resources_build_phase.files_references.any? { |f| + File.basename(f.path.to_s) == plist_name + } + + unless in_resources + target.resources_build_phase.add_file_reference(existing_ref) + puts "Added #{plist_name} to target #{target.name}" + else + puts "#{plist_name} already in target #{target.name}" + end +end + +# 2. Set CODE_SIGN_ENTITLEMENTS on all build configurations for this target +if entitlements_rel_path && !entitlements_rel_path.empty? + target.build_configurations.each do |config| + existing = config.build_settings["CODE_SIGN_ENTITLEMENTS"].to_s + if existing.empty? + config.build_settings["CODE_SIGN_ENTITLEMENTS"] = entitlements_rel_path + puts "Set CODE_SIGN_ENTITLEMENTS=#{entitlements_rel_path} [#{config.name}]" + else + puts "CODE_SIGN_ENTITLEMENTS already set [#{config.name}]: #{existing}" + end + end +end + +project.save +puts "done" diff --git a/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-project.mjs b/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-project.mjs new file mode 100644 index 0000000000..ab78b1ee41 --- /dev/null +++ b/main/.mintlify/skills/auth0-swift/scripts/utils/xcode-project.mjs @@ -0,0 +1,68 @@ +import { $ } from "execa" +import path from "node:path" +import { fileURLToPath } from "node:url" +import ora from "ora" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const RUBY_SCRIPT = path.join(__dirname, "xcode-modify.rb") + +/** + * Ensure the xcodeproj Ruby gem is available, installing it if needed. + * Returns true if the gem is usable, false otherwise. + */ +async function ensureXcodeprojGem() { + try { + await $`ruby -e "require 'xcodeproj'"` + return true + } catch { + const installSpinner = ora("Installing xcodeproj gem").start() + try { + await $({ timeout: 120000 })`gem install xcodeproj --quiet` + installSpinner.succeed("xcodeproj gem installed") + return true + } catch (e) { + installSpinner.fail(`Could not install xcodeproj gem: ${e.message}`) + return false + } + } +} + +/** + * Configure the Xcode project: + * - Adds Auth0.plist to the main app target's Resources build phase. + * - Sets CODE_SIGN_ENTITLEMENTS on all target build configurations. + * + * Falls back to a warning if the xcodeproj gem cannot be loaded. + */ +export async function configureXcodeProject(xcodeprojPath, plistAbsPath, entitlementsRelPath) { + const spinner = ora("Configuring Xcode project").start() + + const gemAvailable = await ensureXcodeprojGem() + if (!gemAvailable) { + spinner.warn( + "xcodeproj gem unavailable — add Auth0.plist to the target and set " + + "CODE_SIGN_ENTITLEMENTS manually in Xcode" + ) + return false + } + + try { + const { stdout, stderr } = await $({ + timeout: 60000, + })`ruby ${RUBY_SCRIPT} ${xcodeprojPath} ${plistAbsPath} ${entitlementsRelPath}` + + if (stderr) process.stderr.write(stderr) + + if (stdout.includes("done")) { + spinner.succeed("Xcode project configured (Auth0.plist in target, CODE_SIGN_ENTITLEMENTS set)") + return true + } + + spinner.warn(`Xcode project configuration incomplete — check output:\n${stdout}`) + return false + } catch (e) { + spinner.fail(`Xcode project configuration failed: ${e.message}`) + // Non-fatal: user can finish manually + return false + } +} diff --git a/main/.mintlify/skills/auth0-vue/SKILL.md b/main/.mintlify/skills/auth0-vue/SKILL.md index c1ee49e148..e82a1b6934 100644 --- a/main/.mintlify/skills/auth0-vue/SKILL.md +++ b/main/.mintlify/skills/auth0-vue/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding authentication to Vue.js 3 applications (login, log license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Auth0 Vue.js Integration @@ -138,6 +142,7 @@ npm run dev - `auth0-quickstart` - Basic Auth0 setup - `auth0-migration` - Migrate from another auth provider - `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-cli` - Manage Auth0 resources from the terminal --- diff --git a/main/.mintlify/skills/auth0-vue/references/api.md b/main/.mintlify/skills/auth0-vue/references/api.md new file mode 100644 index 0000000000..bed6a3dbcf --- /dev/null +++ b/main/.mintlify/skills/auth0-vue/references/api.md @@ -0,0 +1,265 @@ +## Common Patterns + +### Protected Route (Vue Router) + +**Install Vue Router:** +```bash +npm install vue-router +``` + +**Configure router (`src/router/index.ts`):** +```typescript +import { createRouter, createWebHistory } from 'vue-router'; +import { createAuthGuard } from '@auth0/auth0-vue'; +import Home from '../views/Home.vue'; +import Profile from '../views/Profile.vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/profile', + name: 'Profile', + component: Profile, + beforeEnter: createAuthGuard() // Protect this route + } + ] +}); + +export default router; +``` + +**Alternative: Use the exported `authGuard` directly:** +```typescript +import { createRouter, createWebHistory } from 'vue-router'; +import { authGuard } from '@auth0/auth0-vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/profile', + component: () => import('../views/Profile.vue'), + beforeEnter: authGuard // Use the pre-configured guard + } + ] +}); +``` + +--- + +### Get User Profile + +```vue + + + +``` + +--- + +### Call Protected API + +```vue + + + +``` + +**Note:** If calling APIs, add `audience` to your plugin configuration: + +```typescript +app.use( + createAuth0({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://your-api-identifier' // Add this + } + }) +); +``` + +--- + +### Handle Loading and Error States + +```vue + + + +``` + +--- + +### Composition API with Reactive State + +```vue + + + +``` + +--- + +## Configuration Options + +### Complete Plugin Configuration + +```typescript +app.use( + createAuth0({ + domain: 'your-tenant.auth0.com', + clientId: 'your-client-id', + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://your-api-identifier', // For API calls + scope: 'openid profile email', // Default scopes + }, + cacheLocation: 'localstorage', // or 'memory' for stricter security + useRefreshTokens: true, // Enable refresh tokens + }) +); +``` + +--- + +## Testing + +1. Start your dev server: `npm run dev` +2. Click "Login" button +3. Complete Auth0 Universal Login +4. Verify redirect back to your app with user authenticated +5. Navigate to protected routes +6. Click "Logout" and verify user is logged out + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Invalid state" error | Clear browser storage. Ensure `redirect_uri` matches configured callback URL in Auth0 | +| User stuck on loading | Check Auth0 application has correct callback URLs configured | +| API calls fail with 401 | Ensure `audience` is configured in plugin and matches your API identifier | +| Logout doesn't work | Include `returnTo` URL in logout options and configure in Auth0 "Allowed Logout URLs" | +| Router guard loops | Ensure auth guard checks `isLoading` before redirecting | + +--- + +## Security Considerations + +- **Never expose client secret** - Vue is client-side, use only public client credentials +- **Use PKCE** - Enabled by default with @auth0/auth0-vue +- **Validate tokens on backend** - Never trust client-side token validation +- **Use HTTPS in production** - Auth0 requires HTTPS for production redirect URLs +- **Implement proper CORS** - Configure allowed origins in Auth0 application settings + +--- + +## Related Skills + +- `auth0-quickstart` - Initial Auth0 account setup +- `auth0-migration` - Migrate from another auth provider +- `auth0-mfa` - Add Multi-Factor Authentication +- `auth0-organizations` - B2B multi-tenancy support +- `auth0-passkeys` - Add passkey authentication + +--- + +## References + +- [Auth0 Vue SDK Documentation](https://auth0.com/docs/libraries/auth0-vue) +- [Auth0 Vue SDK GitHub](https://github.com/auth0/auth0-vue) +- [Auth0 Vue Quickstart](https://auth0.com/docs/quickstart/spa/vuejs) +- [Vue Router Documentation](https://router.vuejs.org/) diff --git a/main/.mintlify/skills/auth0-vue/references/integration.md b/main/.mintlify/skills/auth0-vue/references/integration.md new file mode 100644 index 0000000000..d099d639ab --- /dev/null +++ b/main/.mintlify/skills/auth0-vue/references/integration.md @@ -0,0 +1,375 @@ +# Auth0 Vue Integration Patterns + +Practical implementation patterns and examples for common use cases. + +--- + +## Protected Routes + +### Navigation Guard + +Create a navigation guard to protect routes: + +```typescript +// src/router/index.ts +import { createRouter, createWebHistory } from 'vue-router'; +import { createAuthGuard } from '@auth0/auth0-vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: () => import('../views/Home.vue') + }, + { + path: '/profile', + component: () => import('../views/Profile.vue'), + beforeEnter: createAuthGuard(app) + } + ] +}); + +export default router; +``` + +### Alternative: Component-Level Guard + +```vue + + + +``` + +--- + +## User Profile + +### Display User Information + +```vue + + + +``` + +--- + +## Calling APIs + +### API Call with Access Token + +```vue + + + +``` + +### Configure Plugin for API Calls + +When calling APIs, add `audience` to your plugin configuration: + +```typescript +// src/main.ts +app.use( + createAuth0({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://your-api-identifier' // Add this + } + }) +); +``` + +--- + +## Error Handling + +### Handle Loading and Error States + +```vue + + + +``` + +--- + +## Composable Patterns + +### Custom Auth Composable + +Create a custom composable for common auth operations: + +```typescript +// src/composables/useAuthHelper.ts +import { computed } from 'vue'; +import { useAuth0 } from '@auth0/auth0-vue'; + +export function useAuthHelper() { + const { + isAuthenticated, + user, + loginWithRedirect, + logout, + getAccessTokenSilently + } = useAuth0(); + + const userName = computed(() => user.value?.name || 'Guest'); + const userEmail = computed(() => user.value?.email || ''); + + const login = () => { + loginWithRedirect(); + }; + + const logoutUser = () => { + logout({ logoutParams: { returnTo: window.location.origin } }); + }; + + const callProtectedApi = async (url: string) => { + const token = await getAccessTokenSilently(); + return fetch(url, { + headers: { Authorization: `Bearer ${token}` } + }); + }; + + return { + isAuthenticated, + userName, + userEmail, + login, + logoutUser, + callProtectedApi + }; +} +``` + +**Usage:** +```vue + + + +``` + +--- + +## Common Issues + +| Issue | Solution | +|-------|----------| +| "Invalid state" error | Clear browser storage and try again. Ensure `redirect_uri` matches configured callback URL | +| User stuck on loading | Check Auth0 application settings have correct callback URLs configured | +| API calls fail with 401 | Ensure `audience` is configured in plugin and matches your API identifier | +| Logout doesn't work | Include `returnTo` URL in logout options and configure in Auth0 "Allowed Logout URLs" | +| CORS errors | Add your application URL to "Allowed Web Origins" in Auth0 application settings | +| Composables not reactive | Ensure you're accessing `.value` on refs returned from `useAuth0()` | + +--- + +## Security Considerations + +### Client-Side Security + +- **Never expose client secret** - Vue runs client-side, use only public client credentials +- **Use PKCE** - Enabled by default with @auth0/auth0-vue +- **Validate tokens on backend** - Never trust client-side token validation +- **Use HTTPS in production** - Auth0 requires HTTPS for production redirect URLs +- **Implement proper CORS** - Configure allowed origins in Auth0 application settings + +### Token Storage + +The SDK stores tokens in memory by default (cleared on page refresh). To persist sessions: + +```typescript +app.use( + createAuth0({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + cacheLocation: 'localstorage', // or 'memory' for stricter security + useRefreshTokens: true + }) +); +``` + +--- + +## Advanced Patterns + +### Custom Login with Options + +```typescript +import { useAuth0 } from '@auth0/auth0-vue'; + +const { loginWithRedirect } = useAuth0(); + +// Login with specific connection +await loginWithRedirect({ + authorizationParams: { + connection: 'google-oauth2' + } +}); + +// Login with signup screen +await loginWithRedirect({ + authorizationParams: { + screen_hint: 'signup' + } +}); +``` + +### Handle Redirect Callback + +```vue + +``` + +--- + +## Testing + +### Manual Testing Checklist + +1. **Login Flow** + - Start dev server: `npm run dev` + - Click "Login" button + - Complete Auth0 Universal Login + - Verify redirect back to app with user authenticated + +2. **Logout Flow** + - Click "Logout" button + - Verify user is logged out + - Verify redirect to correct page + +3. **Protected Routes** + - Navigate to protected route while logged out + - Verify redirect to Auth0 login + - After login, verify redirect back to protected route + +4. **API Calls** + - Call protected API endpoint + - Verify access token is included + - Verify API responds correctly + +--- + +## Next Steps + +- [API Reference](api.md) - Complete SDK documentation +- [Setup Guide](setup.md) - Detailed setup instructions +- [Main Skill](../SKILL.md) - Return to main skill guide diff --git a/main/.mintlify/skills/auth0-vue/references/setup.md b/main/.mintlify/skills/auth0-vue/references/setup.md new file mode 100644 index 0000000000..2fdc80ccc8 --- /dev/null +++ b/main/.mintlify/skills/auth0-vue/references/setup.md @@ -0,0 +1,286 @@ +# Auth0 Vue Setup Guide + +Complete setup instructions with automated scripts and manual configuration options. + +--- + +## Quick Setup (Automated) + +**Never read the contents of `.env` at any point during setup.** The file may contain sensitive secrets that should not be exposed in the LLM context. If you determine you need to read the file for any reason, ask the user for explicit permission before doing so — do not proceed until the user confirms. + +**Before running any part of this setup that writes to `.env`, you MUST ask the user for explicit confirmation.** Follow the steps below precisely. + +### Step 1: Check for existing .env and confirm with user + +Before writing to `.env`, check whether the file already exists: + +```bash +test -f .env && echo "EXISTS" || echo "NOT_FOUND" +``` + +Then ask the user for explicit confirmation before proceeding — do not continue until the user confirms: + +- If `.env` does **not** exist, ask: + - Question: "This setup will create a `.env` file containing Auth0 credentials (domain and client ID). Do you want to proceed?" + - Options: "Yes, create .env" / "No, I'll configure it manually" + +- If `.env` **already exists**, ask: + - Question: "A `.env` file already exists and may contain secrets unrelated to Auth0. This setup will append Auth0 credentials to it without modifying existing content. Do you want to proceed?" + - Options: "Yes, append to existing .env" / "No, I'll update it manually" + +**Do not proceed with writing to `.env` unless the user selects the confirmation option.** + +### Step 2: Run automated setup (only after confirmation) + +#### Bash Script (macOS/Linux) + +Run this script to automatically set up everything: + +```bash +#!/bin/bash + +# Detect OS and install Auth0 CLI if needed +if ! command -v auth0 &> /dev/null; then + echo "Installing Auth0 CLI..." + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install auth0/auth0-cli/auth0 + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Download and review the install script before executing + curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh + echo "⚠️ Review the install script at /tmp/auth0-install.sh before running" + sh /tmp/auth0-install.sh -b /usr/local/bin + rm /tmp/auth0-install.sh + fi +fi + +# Check if logged in to Auth0 +if ! auth0 tenants list &> /dev/null; then + echo "======================================" + echo "Auth0 Login Required" + echo "======================================" + read -p "Do you have an Auth0 account? (y/n): " HAS_ACCOUNT + + if [[ "$HAS_ACCOUNT" != "y" ]]; then + echo "Let's create your free Auth0 account!" + echo "1. Visit: https://auth0.com/signup" + echo "2. Sign up with your email or GitHub" + echo "3. Choose a tenant domain" + read -p "Press Enter when you've created your account..." + fi + + auth0 login +fi + +# List apps and prompt for selection +echo "Your Auth0 applications:" +auth0 apps list + +read -p "Enter your Auth0 app ID (or press Enter to create new): " APP_ID + +if [ -z "$APP_ID" ]; then + echo "Creating new Auth0 SPA application..." + APP_NAME="${PWD##*/}-vue-app" + APP_ID=$(auth0 apps create \ + --name "$APP_NAME" \ + --type spa \ + --auth-method None \ + --callbacks "http://localhost:5173,http://localhost:3000" \ + --logout-urls "http://localhost:5173,http://localhost:3000" \ + --origins "http://localhost:5173,http://localhost:3000" \ + --web-origins "http://localhost:5173,http://localhost:3000" \ + --metadata "created_by=agent_skills" \ + --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + echo "Created app with ID: $APP_ID" +fi + +# Get app details and create .env file +echo "Fetching Auth0 credentials..." +AUTH0_DOMAIN=$(auth0 apps show "$APP_ID" --json | grep -o '"domain":"[^"]*' | cut -d'"' -f4) +AUTH0_CLIENT_ID=$(auth0 apps show "$APP_ID" --json | grep -o '"client_id":"[^"]*' | cut -d'"' -f4) + +# Append Auth0 credentials to .env +cat >> .env << EOF +VITE_AUTH0_DOMAIN=$AUTH0_DOMAIN +VITE_AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID +EOF + +echo "✅ Auth0 configuration complete!" +echo "Appended to .env:" +echo " VITE_AUTH0_DOMAIN=$AUTH0_DOMAIN" +echo " VITE_AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID" +``` + +#### PowerShell Script (Windows) + +```powershell +# Install Auth0 CLI if not present +if (!(Get-Command auth0 -ErrorAction SilentlyContinue)) { + Write-Host "Installing Auth0 CLI..." + scoop install auth0 +} + +# Check if logged in +try { + auth0 tenants list | Out-Null +} catch { + Write-Host "======================================" + Write-Host "Auth0 Login Required" + Write-Host "======================================" + + $hasAccount = Read-Host "Do you have an Auth0 account? (y/n)" + + if ($hasAccount -ne "y") { + Write-Host "Let's create your free Auth0 account!" + Write-Host "1. Visit: https://auth0.com/signup" + Write-Host "2. Sign up with your email or GitHub" + Read-Host "Press Enter when you've created your account" + } + + auth0 login +} + +# List and select app +Write-Host "Your Auth0 applications:" +auth0 apps list + +$appId = Read-Host "Enter your Auth0 app ID (or press Enter to create new)" + +if ([string]::IsNullOrEmpty($appId)) { + $appName = Split-Path -Leaf (Get-Location) + Write-Host "Creating new Auth0 SPA application..." + $appJson = auth0 apps create --name "$appName-vue-app" --type spa ` + --auth-method None ` + --callbacks "http://localhost:5173,http://localhost:3000" ` + --logout-urls "http://localhost:5173,http://localhost:3000" ` + --origins "http://localhost:5173,http://localhost:3000" ` + --web-origins "http://localhost:5173,http://localhost:3000" ` + --metadata "created_by=agent_skills" --json + + $appId = ($appJson | ConvertFrom-Json).client_id + Write-Host "Created app with ID: $appId" +} + +# Get credentials and create .env +Write-Host "Fetching Auth0 credentials..." +$appDetails = auth0 apps show $appId --json | ConvertFrom-Json + +@" +VITE_AUTH0_DOMAIN=$($appDetails.domain) +VITE_AUTH0_CLIENT_ID=$($appDetails.client_id) +"@ | Out-File -FilePath .env -Encoding UTF8 -Append + +Write-Host "✅ Auth0 configuration complete!" +Write-Host "Appended to .env:" +Write-Host " VITE_AUTH0_DOMAIN=$($appDetails.domain)" +Write-Host " VITE_AUTH0_CLIENT_ID=$($appDetails.client_id)" +``` + +--- + +## Manual Setup + +### Step 1: Install SDK + +```bash +npm install @auth0/auth0-vue +``` + +### Step 2: Install Auth0 CLI + +**macOS:** +```bash +brew install auth0/auth0-cli/auth0 +``` + +**Linux (review script before executing):** +```bash +curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh -o /tmp/auth0-install.sh +# Review the script before running: cat /tmp/auth0-install.sh +sh /tmp/auth0-install.sh +rm /tmp/auth0-install.sh +``` + +**Windows:** +```powershell +scoop install auth0 +``` + +### Step 3: Get Credentials + +```bash +# Login to Auth0 +auth0 login + +# List your apps +auth0 apps list + +# Get app details +auth0 apps show +``` + +### Step 4: Create .env File + +```bash +VITE_AUTH0_DOMAIN=.auth0.com +VITE_AUTH0_CLIENT_ID= +``` + +--- + +## Creating Auth0 Application via Dashboard + +1. Go to [Auth0 Dashboard](https://manage.auth0.com) +2. Navigate to **Applications** → **Applications** +3. Click **Create Application** +4. Choose **Single Page Web Applications** +5. Configure: + - **Allowed Callback URLs**: `http://localhost:5173, http://localhost:3000` + - **Allowed Logout URLs**: `http://localhost:5173, http://localhost:3000` + - **Allowed Web Origins**: `http://localhost:5173, http://localhost:3000` + - **Allowed Origins (CORS)**: `http://localhost:5173, http://localhost:3000` +6. Copy your **Domain** and **Client ID** +7. Create `.env` file as shown above + +--- + +## Troubleshooting + +### Environment Variables Not Loading + +**Issue**: Variables not available in app + +**Solution:** +- Ensure variables start with `VITE_` prefix +- Restart dev server after creating `.env` +- Check file is named exactly `.env` (not `.env.local`) +- Vite only loads variables at build time, not runtime + +### Auth0 CLI Issues + +**Browser doesn't open:** +```bash +auth0 login --no-browser +``` + +**"Not logged in" error:** +```bash +auth0 login --force +``` + +### CORS Errors + +**Issue**: CORS errors when logging in + +**Solution:** +- Add your app URL to "Allowed Web Origins" in Auth0 Dashboard +- Ensure callback URLs include protocol (`http://` or `https://`) +- For local dev, use `http://localhost:5173` (Vite default) + +--- + +## Next Steps + +After setup is complete: +1. Return to [main skill guide](../SKILL.md) for integration steps +2. See [Integration Guide](integration.md) for advanced patterns +3. Check [API Reference](api.md) for complete SDK documentation diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/SKILL.md b/main/.mintlify/skills/express-oauth2-jwt-bearer/SKILL.md index 7752ac91b0..064453f577 100644 --- a/main/.mintlify/skills/express-oauth2-jwt-bearer/SKILL.md +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/SKILL.md @@ -4,6 +4,10 @@ description: Use when adding Auth0 token validation to Express or Node.js APIs - license: Apache-2.0 metadata: author: Auth0 + version: '1.0.0' + openclaw: + emoji: "\U0001F510" + homepage: https://github.com/auth0/agent-skills --- # Node OAuth2 JWT Bearer Integration @@ -117,6 +121,7 @@ The `express-oauth2-jwt-bearer` package provides Express middleware for validati - **[auth0-aspnetcore-api](../auth0-aspnetcore-api)** — BACKEND_API reference implementation for .NET - **[go-jwt-middleware](../go-jwt-middleware)** — JWT middleware for Go APIs - **[auth0-api-python](../auth0-api-python)** — JWT validation for Python APIs (Flask/FastAPI) +- **[auth0-cli](../auth0-cli)** — Manage Auth0 resources from the terminal ## Quick Reference diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/references/api.md b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/api.md new file mode 100644 index 0000000000..0e60b23f05 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/api.md @@ -0,0 +1,216 @@ +# express-oauth2-jwt-bearer API Reference & Testing + +## Configuration Reference + +All options are passed to the `auth()` function or set via environment variables. + +### auth() Options + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `issuerBaseURL` | `string` | Yes (or `ISSUER_BASE_URL` env var) | — | Auth0 domain with `https://`, e.g. `https://your-tenant.us.auth0.com` | +| `audience` | `string` | Yes (or `AUDIENCE` env var) | — | API Identifier from Auth0 Dashboard, e.g. `https://my-api.com` | +| `secret` | `string` | For HS256 only | — | Shared secret for symmetric JWT signing (HS256). Not required for RS256. | +| `tokenSigningAlg` | `string` | No | `RS256` | JWT signing algorithm. Use `HS256` for symmetric keys. | +| `issuer` | `string` | No (alternative to `issuerBaseURL`) | — | Issuer claim value — use with `jwksUri` for non-standard setups | +| `jwksUri` | `string` | No | Derived from `issuerBaseURL` | Custom JWKS endpoint URL | +| `authRequired` | `boolean` | No | `true` | Set `false` to allow unauthenticated requests through (attach auth info if present) | +| `clockTolerance` | `number` | No | `(none)` | Clock skew tolerance in seconds (undefined unless explicitly set) | +| `validators` | `Validators` | No | — | Custom validator overrides. Set `{ iss: false }` to skip issuer validation. | +| `dpop` | `DPoPOptions` | No | — | DPoP configuration (see below) | + +### DPoPOptions + +| Option | Type | Description | +|--------|------|-------------| +| `enabled` | `boolean` | Enable DPoP token binding. Default is `true` (hybrid Bearer+DPoP mode). | +| `required` | `boolean` | Set `true` to reject plain Bearer tokens (DPoP-only mode). Default: `false`. | +| `iatOffset` | `number` | Max age of a DPoP proof in seconds. | +| `iatLeeway` | `number` | Leeway for `iat` claim in DPoP proofs. | + +### Environment Variables (auto-detected) + +When no options are passed to `auth()`, these variables are read automatically: + +| Variable | Description | +|----------|-------------| +| `ISSUER_BASE_URL` | Auth0 domain with `https://` prefix: `https://your-tenant.us.auth0.com` | +| `AUDIENCE` | API Identifier: `https://your-api.example.com` | + +**Note:** `AUTH0_DOMAIN` / `AUTH0_AUDIENCE` are the conventional `.env` keys used in this skill. Pass them explicitly: +```javascript +auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, +}) +``` + +## Claims Reference + +| Claim | Type | Description | +|-------|------|-------------| +| `sub` | `string` | Subject identifier — the user's or M2M app's unique Auth0 ID | +| `iss` | `string` | Issuer — your Auth0 tenant URL (e.g. `https://your-tenant.us.auth0.com/`) | +| `aud` | `string \| string[]` | Audience — must match your API Identifier | +| `exp` | `number` | Expiration timestamp (Unix epoch) | +| `iat` | `number` | Issued-at timestamp (Unix epoch) | +| `scope` | `string` | Space-separated scopes granted to the token | +| `permissions` | `string[]` | Array of RBAC permissions (Auth0-specific, enabled via RBAC settings on the API) | +| `azp` | `string` | Authorized party — client ID of the application that requested the token | +| `org_id` | `string` | Organization ID (Auth0 Organizations feature) | + +**Accessing claims in a handler:** +```javascript +app.get('/api/me', checkJwt, (req, res) => { + const { sub, permissions, scope } = req.auth.payload; + res.json({ sub, permissions }); +}); +``` + +## Code Examples + +### Complete minimal example + +```javascript +// server.js +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import { auth, requiredScopes, claimIncludes } from 'express-oauth2-jwt-bearer'; + +const app = express(); + +// 1. CORS before auth (required for preflight requests) +app.use(cors({ + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + allowedHeaders: ['Authorization', 'Content-Type', 'DPoP'], +})); + +app.use(express.json()); + +// 2. JWT validation middleware +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, +}); + +// 3. Public endpoint (no auth required) +app.get('/api/public', (req, res) => { + res.json({ message: 'This is a public endpoint' }); +}); + +// 4. Private endpoint (JWT required) +app.get('/api/private', checkJwt, (req, res) => { + res.json({ + message: 'Authenticated', + sub: req.auth.payload.sub, + }); +}); + +// 5. Scoped endpoint (specific scope required) +app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => { + res.json({ messages: ['Hello', 'World'] }); +}); + +// 6. Permission-based endpoint (RBAC permissions claim) +app.get('/api/admin', checkJwt, claimIncludes('permissions', 'admin:access'), (req, res) => { + res.json({ message: 'Admin access granted' }); +}); + +// 7. RFC 6750 error handler +app.use((err, req, res, next) => { + if (err.status) { + return res.status(err.status).json({ + error: err.code, + message: err.message, + }); + } + next(err); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => console.log(`API listening on port ${PORT}`)); +``` + +### Environment configuration (.env) + +```env +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_AUDIENCE=https://your-api.example.com +PORT=3000 +CORS_ORIGIN=http://localhost:5173 +``` + +### TypeScript example + +```typescript +import 'dotenv/config'; +import express, { Request, Response, NextFunction } from 'express'; +import { auth, requiredScopes } from 'express-oauth2-jwt-bearer'; + +// Note: express-oauth2-jwt-bearer already declares req.auth on the Express +// Request interface in its own .d.ts — no need to redeclare it here. + +const app = express(); + +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, +}); + +app.get('/api/private', checkJwt, (req: Request, res: Response) => { + const sub = req.auth?.payload.sub; + res.json({ sub }); +}); +``` + +## Testing Checklist + +- [ ] **Public endpoint** returns `200` without a token: `curl http://localhost:3000/api/public` +- [ ] **Protected endpoint** returns `401` without a token: `curl http://localhost:3000/api/private` +- [ ] **Protected endpoint** returns `200` with valid M2M token: `curl -H "Authorization: Bearer " http://localhost:3000/api/private` +- [ ] **Scoped endpoint** returns `403` with token missing required scope +- [ ] **Scoped endpoint** returns `200` with token that has the required scope +- [ ] **Expired token** returns `401` with error description +- [ ] **Wrong audience** returns `401` +- [ ] **CORS preflight** (`OPTIONS`) returns `200` from protected routes +- [ ] `req.auth.payload.sub` contains the expected subject +- [ ] `req.auth.payload.permissions` array is populated (if RBAC is enabled on the Auth0 API) + +### Getting a test token with M2M credentials + +```bash +curl --request POST \ + --url "https://YOUR_AUTH0_DOMAIN/oauth/token" \ + --header "content-type: application/json" \ + --data '{ + "client_id": "YOUR_M2M_CLIENT_ID", + "client_secret": "YOUR_M2M_CLIENT_SECRET", + "audience": "YOUR_API_AUDIENCE", + "grant_type": "client_credentials" + }' +``` + +## Common Issues + +| Error | Cause | Fix | +|-------|-------|-----| +| `UnauthorizedError: No authorization token was found` | No `Authorization: Bearer ...` header | Add the bearer token to the request header | +| `UnauthorizedError: invalid_token — jwt audience invalid` | Audience mismatch | Verify `AUTH0_AUDIENCE` matches the API Identifier in Auth0 Dashboard exactly | +| `UnauthorizedError: invalid_token — jwt issuer invalid` | Domain mismatch | Verify `AUTH0_DOMAIN` is the Auth0 tenant hostname (no `https://`) | +| `UnauthorizedError: invalid_token — jwt expired` | Token has expired | Request a new token; check system clock drift (`clockTolerance` option) | +| `Error: JWKS request failed` | Network or domain misconfiguration | Verify `AUTH0_DOMAIN` is reachable; check network/proxy settings | +| `InsufficientScopeError: Insufficient scope` | Token lacks required scope | Verify the requesting app has the scope granted; check `requiredScopes()` call | +| `CORS error` on OPTIONS preflight | Auth middleware running before CORS | Move `cors()` middleware before `auth()` in the middleware chain | +| `TypeError: Cannot read properties of undefined (reading 'payload')` | `req.auth` is undefined | Check that `checkJwt` middleware runs before the handler | + +## Security Considerations + +- **Never log tokens.** Full JWT strings contain sensitive claims. Log only `sub` or `jti` for tracing. +- **CORS before auth.** Always register `cors()` before `auth()`. Auth middleware rejects OPTIONS preflight requests with 401 if CORS isn't set first. +- **Audience validation is critical.** Without a matching `audience`, your API would accept tokens issued for other services. +- **Issuer validation.** The `issuerBaseURL` is used to fetch the JWKS and validate the `iss` claim. Never disable issuer validation in production. +- **RBAC via `permissions` claim.** Auth0 RBAC stores user permissions in the `permissions` JWT claim (not `scope`). Enable "Add Permissions in the Access Token" on your Auth0 API settings. +- **DPoP.** For APIs requiring sender-constrained tokens, enable DPoP with `dpop: { enabled: true, required: true }`. This prevents token theft — stolen tokens cannot be replayed without the original private key. +- **Helmet.** Pair with `helmet` for security headers: `npm install helmet` + `app.use(helmet())`. +- **Production secrets.** Never commit `.env` to source control. Use environment variables in production (Railway, Heroku, Fly.io, etc.). diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/references/integration.md b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/integration.md new file mode 100644 index 0000000000..02af87d885 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/integration.md @@ -0,0 +1,325 @@ +# Integration Patterns + +## Authentication Flow + +```text +Client → API + 1. Client obtains access token from Auth0 (via /oauth/token) + 2. Client sends request with "Authorization: Bearer " header + 3. express-oauth2-jwt-bearer middleware: + a. Extracts bearer token from Authorization header + b. Fetches public key from Auth0 JWKS endpoint (cached) + c. Verifies token signature, issuer, audience, expiry + d. Attaches decoded token to req.auth + 4. Route handler accesses req.auth.payload +``` + +## Protected Endpoints + +### Global protection + +Apply `checkJwt` middleware globally to protect all routes: + +```javascript +import { auth } from 'express-oauth2-jwt-bearer'; + +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, +}); + +// All routes below this require a valid JWT +app.use(checkJwt); +app.get('/api/users', (req, res) => { + res.json({ sub: req.auth.payload.sub }); +}); +``` + +### Per-route protection + +Apply middleware to specific routes only: + +```javascript +// Public — no auth +app.get('/api/public', (req, res) => { + res.json({ message: 'Public endpoint' }); +}); + +// Protected — JWT required +app.get('/api/private', checkJwt, (req, res) => { + res.json({ sub: req.auth.payload.sub }); +}); +``` + +### Optional authentication + +Allow unauthenticated requests but attach auth info when present: + +```javascript +const optionalAuth = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, + authRequired: false, +}); + +app.get('/api/profile', optionalAuth, (req, res) => { + if (req.auth) { + res.json({ sub: req.auth.payload.sub, authenticated: true }); + } else { + res.json({ authenticated: false }); + } +}); +``` + +## RBAC — Scope-Based Authorization + +Use `requiredScopes()` to enforce scopes on access tokens: + +```javascript +import { auth, requiredScopes } from 'express-oauth2-jwt-bearer'; + +// All scopes must be present +app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => { + res.json({ messages: [] }); +}); + +// Multiple scopes required +app.post('/api/messages', checkJwt, requiredScopes('read:messages write:messages'), (req, res) => { + res.json({ created: true }); +}); +``` + +### Permission-based RBAC (Auth0 RBAC feature) + +When Auth0 RBAC is enabled on the API, permissions are stored in the `permissions` claim: + +```javascript +import { auth, claimIncludes } from 'express-oauth2-jwt-bearer'; + +// Require 'read:messages' in the permissions claim +app.get('/api/messages', checkJwt, claimIncludes('permissions', 'read:messages'), (req, res) => { + res.json({ messages: [] }); +}); + +// Require multiple permissions +app.delete('/api/messages/:id', checkJwt, claimIncludes('permissions', 'delete:messages'), (req, res) => { + res.json({ deleted: true }); +}); +``` + +## Claim Validation + +### claimEquals — exact value match + +```javascript +import { auth, claimEquals } from 'express-oauth2-jwt-bearer'; + +// Require org_id to equal a specific value +app.get('/api/org-data', checkJwt, claimEquals('org_id', 'org_123'), (req, res) => { + res.json({ org: 'org_123' }); +}); +``` + +### claimIncludes — array contains all values + +```javascript +import { auth, claimIncludes } from 'express-oauth2-jwt-bearer'; + +// Require the roles claim to include 'admin' +app.get('/api/admin', checkJwt, claimIncludes('roles', 'admin'), (req, res) => { + res.json({ admin: true }); +}); +``` + +### claimCheck — custom validation logic + +```javascript +import { auth, claimCheck } from 'express-oauth2-jwt-bearer'; + +// Custom validation function +app.get('/api/premium', checkJwt, claimCheck((payload) => { + return payload?.subscription === 'premium' && payload?.active === true; +}, 'Premium subscription required'), (req, res) => { + res.json({ premium: true }); +}); +``` + +## CORS Configuration + +**Critical:** CORS middleware must come before auth middleware. Auth rejects OPTIONS preflight requests with 401 if CORS isn't configured first. + +```javascript +import cors from 'cors'; +import { auth } from 'express-oauth2-jwt-bearer'; + +// 1. CORS first (handles OPTIONS preflight) +app.use(cors({ + origin: 'http://localhost:5173', // Your frontend URL + allowedHeaders: ['Authorization', 'Content-Type', 'DPoP'], + exposedHeaders: ['WWW-Authenticate'], +})); + +// 2. Auth second +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, +}); +``` + +## DPoP Support + +DPoP (Demonstration of Proof-of-Possession) binds tokens to the client's key pair, preventing token theft. The SDK supports DPoP natively. + +### Hybrid mode (Bearer or DPoP both accepted — default) + +```javascript +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, + dpop: { + enabled: true, + required: false, // Accept both Bearer and DPoP tokens + }, +}); +``` + +### DPoP-only mode (rejects plain Bearer tokens) + +```javascript +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, + dpop: { + enabled: true, + required: true, // Reject plain Bearer tokens + }, +}); +``` + +### Bearer-only mode (disable DPoP) + +```javascript +const checkJwt = auth({ + issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, + audience: process.env.AUTH0_AUDIENCE, + dpop: { enabled: false }, +}); +``` + + +## Error Handling + +The SDK throws RFC 6750-compliant errors with `.status` and `.headers` properties. Add an error handler after your routes: + +```javascript +app.use((err, req, res, next) => { + if (err.status) { + // JWT validation error — send WWW-Authenticate header per RFC 6750 + res.set(err.headers); + return res.status(err.status).json({ + error: err.code, + error_description: process.env.NODE_ENV === 'production' ? undefined : err.message, + }); + } + // Other errors + console.error(err); + res.status(500).json({ error: 'internal_error' }); +}); +``` + +### Error types + +| Error Class | Status | Code | Cause | +|------------|--------|------|-------| +| `UnauthorizedError` | 401 | `invalid_token` | Missing, expired, or malformed token | +| `InvalidRequestError` | 400 | `invalid_request` | Malformed Authorization header | +| `InvalidTokenError` | 401 | `invalid_token` | Token signature/claims validation failed | +| `InsufficientScopeError` | 403 | `insufficient_scope` | Token lacks required scope | + +```javascript +import { + UnauthorizedError, + InvalidTokenError, + InsufficientScopeError +} from 'express-oauth2-jwt-bearer'; + +app.use((err, req, res, next) => { + if (err instanceof UnauthorizedError || err instanceof InvalidTokenError) { + return res.status(401).json({ error: 'unauthorized' }); + } + if (err instanceof InsufficientScopeError) { + return res.status(403).json({ error: 'forbidden' }); + } + next(err); +}); +``` + +## Testing Patterns + +### Manual testing with curl + +```bash +# 1. Get a test token (from Auth0 Dashboard → APIs → Test, or via M2M credentials) +ACCESS_TOKEN=$(curl -s --request POST \ + --url "https://YOUR_AUTH0_DOMAIN/oauth/token" \ + --header "content-type: application/json" \ + --data '{ + "client_id": "YOUR_M2M_CLIENT_ID", + "client_secret": "YOUR_M2M_CLIENT_SECRET", + "audience": "YOUR_API_AUDIENCE", + "grant_type": "client_credentials" + }' | jq -r '.access_token') + +# 2. Test protected endpoint +curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/api/private + +# 3. Test scoped endpoint +curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/api/messages +``` + +### Unit testing with Jest/Vitest + +```javascript +import request from 'supertest'; +import app from './app.js'; + +describe('API Authentication', () => { + it('returns 401 without token', async () => { + const res = await request(app).get('/api/private'); + expect(res.status).toBe(401); + }); + + it('returns 200 with valid token', async () => { + // Use a test token from Auth0 or a mocked JWT + const res = await request(app) + .get('/api/private') + .set('Authorization', `Bearer ${process.env.TEST_TOKEN}`); + expect(res.status).toBe(200); + }); +}); +``` + +### Mocking in unit tests + +For unit tests, you can mock the `auth` middleware to bypass JWT validation: + +```javascript +// test-utils.js +import { jest } from '@jest/globals'; + +export function mockAuth(payload = { sub: 'test-user' }) { + jest.mock('express-oauth2-jwt-bearer', () => ({ + auth: () => (req, res, next) => { + req.auth = { payload }; + next(); + }, + requiredScopes: (scopes) => (req, res, next) => { + const tokenScopes = req.auth?.payload?.scope?.split(' ') || []; + const missing = [scopes].flat().filter(s => !tokenScopes.includes(s)); + if (missing.length) return res.status(403).json({ error: 'insufficient_scope' }); + next(); + }, + claimIncludes: () => (req, res, next) => next(), + })); +} +``` diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/references/setup.md b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/setup.md new file mode 100644 index 0000000000..b146933546 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/references/setup.md @@ -0,0 +1,183 @@ +# express-oauth2-jwt-bearer Setup Guide + +## Auth0 Configuration + +> **Agent instruction:** +> +> **Check if credentials are already provided in the user's prompt:** If the user's prompt already includes Auth0 Domain and API Audience (e.g. `your-tenant.us.auth0.com` and `https://api.example.com`), use them directly — skip to "Write the .env file" below. Do NOT call `AskUserQuestion` to re-confirm provided credentials, and do NOT run the bootstrap script. +> +> If credentials are NOT provided, offer setup choices: +> +> Use `AskUserQuestion` to ask the user: +> "How would you like to configure Auth0 for this project?" +> - Option A: **Automatic setup (recommended)** — runs the bootstrap script to create the Auth0 API automatically +> - Option B: **Manual setup** — provide Auth0 credentials manually +> +> **If Automatic Setup (Option A):** +> +> 1. **Pre-flight checks:** +> - Verify Node.js 20+: `node --version` +> - Verify Auth0 CLI installed: `auth0 --version` +> - Verify logged in: `auth0 tenants list --csv --no-input` +> - If any check fails, guide user to install/login, or fall back to Option B +> +> 2. **Run bootstrap script:** +> ```bash +> cd /scripts && npm install && node bootstrap.mjs +> ``` +> The script will: +> - Validate the project structure (detect `package.json` with Node.js API patterns) +> - Discover existing Auth0 APIs +> - Show a change plan (CREATE or SKIP) +> - Create the Auth0 API (Resource Server) with the specified identifier +> - Write the `.env` configuration file with Domain + Audience +> - Print a summary +> +> **If Manual Setup (Option B):** +> +> Ask the user for: +> - **Auth0 Domain** (e.g., `your-tenant.us.auth0.com`) +> - **API Audience** — the API Identifier you set when creating the Auth0 API (e.g., `https://your-api.example.com`) +> +> Then write the `.env` file (see below). +> +> **Write the .env file** (both paths): +> ```env +> AUTH0_DOMAIN=your-tenant.us.auth0.com +> AUTH0_AUDIENCE=https://your-api.example.com +> PORT=3000 +> ``` + +### Auth0 API Registration (Resource Server) + +The bootstrap script automatically runs `auth0 apis create` to register your API as a Resource Server. This produces the `AUTH0_AUDIENCE` value (the API Identifier) that your middleware uses for token validation. + +**Auth0 CLI command (for reference):** +```bash +auth0 apis create \ + --name "My Node API" \ + --identifier "https://my-api.example.com" \ + --json --no-input +``` + +### Creating the Auth0 API manually (if not using bootstrap script) + +1. Go to [Auth0 Dashboard → APIs](https://manage.auth0.com/#/apis) +2. Click **Create API** +3. Set: + - **Name**: Your API name (e.g., "My Node API") + - **Identifier**: A URL-like identifier (e.g., `https://my-api.example.com`) — this becomes `AUTH0_AUDIENCE` + - **Signing Algorithm**: `RS256` (recommended) +4. Click **Create** +5. Note the **API Identifier** — this is your Audience value + +### Enable RBAC (optional) + +To use `claimIncludes('permissions', 'read:data')` with Auth0 RBAC: + +1. Go to Auth0 Dashboard → APIs → your API → Settings +2. Enable **"Enable RBAC"** +3. Enable **"Add Permissions in the Access Token"** +4. Add permissions under the **Permissions** tab +5. Assign permissions to roles, and roles to users via Auth0 Dashboard + +## Post-Setup Steps + +After running the bootstrap script or manual setup: + +1. **Verify domain and audience** are correct in `.env` +2. **Test the API is reachable**: `auth0 apis list --json --no-input | grep your-api` +3. **Confirm CORS is configured** before auth middleware in your server file (see integration.md) +4. **Request a test token** using M2M credentials or the Auth0 Dashboard test feature: + - Go to Auth0 Dashboard → APIs → your API → Test tab + - Click **Copy Token** to get a test access token + +## SDK Installation + +```bash +npm install express-oauth2-jwt-bearer +``` + +**With additional recommended packages:** +```bash +npm install express-oauth2-jwt-bearer dotenv cors helmet +npm install --save-dev @types/express @types/cors # TypeScript projects +``` + +**package.json dependency:** +```json +{ + "dependencies": { + "express-oauth2-jwt-bearer": "^1.7.4", + "dotenv": "^16.0.0", + "cors": "^2.8.5", + "helmet": "^7.0.0" + } +} +``` + +## Secret Management + +`express-oauth2-jwt-bearer` requires only **Domain** and **Audience** — no Client Secret. The middleware validates tokens using the Auth0 JWKS (JSON Web Key Set) endpoint, which provides the public signing keys. This means: + +- **No client secret needed** for token validation +- The JWKS endpoint is publicly accessible at `https://{AUTH0_DOMAIN}/.well-known/jwks.json` +- The middleware fetches and caches keys automatically + +### .env file (development) + +```env +# .env — Never commit to source control +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_AUDIENCE=https://your-api.example.com +PORT=3000 +``` + +### Production environment variables + +Set these as environment variables in your hosting platform (not in `.env` files): + +| Variable | Example Value | +|----------|--------------| +| `AUTH0_DOMAIN` | `your-tenant.us.auth0.com` | +| `AUTH0_AUDIENCE` | `https://your-api.example.com` | +| `PORT` | `3000` | + +**Never commit `.env` to source control.** Add `.env` to `.gitignore`: +```bash +echo ".env" >> .gitignore +``` + +**Load `.env` in your entry file:** +```javascript +import 'dotenv/config'; // must be at the top +// or: require('dotenv').config(); +``` + +## Verification + +After setup, verify everything is working: + +1. **Start the server:** + ```bash + node server.js + # or: npm start + ``` + +2. **Test public endpoint:** + ```bash + curl http://localhost:3000/api/public + # Expected: 200 OK + ``` + +3. **Test protected endpoint without token:** + ```bash + curl http://localhost:3000/api/private + # Expected: 401 Unauthorized + ``` + +4. **Get a test token** from Auth0 Dashboard → APIs → your API → Test tab, then: + ```bash + curl -H "Authorization: Bearer " http://localhost:3000/api/private + # Expected: 200 OK with payload data + ``` diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/bootstrap.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/bootstrap.mjs new file mode 100644 index 0000000000..8ad2fc7fee --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/bootstrap.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +import path from "node:path" + +import { + checkNodeVersion, + checkAuth0CLI, + getActiveTenant, + validateApiProject, +} from "./utils/validation.mjs" +import { + discoverExistingApis, + buildChangePlan, + displayChangePlan, +} from "./utils/discovery.mjs" +import { applyApiChanges } from "./utils/apis.mjs" +import { writeEnvFile } from "./utils/env-writer.mjs" +import { confirmWithUser } from "./utils/helpers.mjs" + +async function main() { + console.log("\n Auth0 Express API Bootstrap\n") + + const projectPath = path.resolve(process.argv[2] || process.cwd()) + + // Pre-flight + checkNodeVersion() + await checkAuth0CLI() + const domain = await getActiveTenant() + + // Validate project + const config = validateApiProject(projectPath) + + // Discover + plan + const apis = await discoverExistingApis() + const plan = buildChangePlan(apis, domain, config) + displayChangePlan(plan) + + // Confirm + const confirmed = await confirmWithUser("Apply these changes?") + if (!confirmed) { + console.log("\n Aborted by user.\n") + process.exit(0) + } + + // Execute + console.log("") + const api = await applyApiChanges(plan.api) + + const envPath = path.join(projectPath, ".env") + await writeEnvFile( + { + AUTH0_DOMAIN: domain, + AUTH0_AUDIENCE: api.identifier, + }, + envPath + ) + + // Summary + console.log("\n Auth0 Express API Setup Complete\n") + console.log(` Domain: ${domain}`) + console.log(` Audience: ${api.identifier}`) + console.log("") + console.log(" Next steps:") + console.log(" 1. Install SDK: npm install express-oauth2-jwt-bearer dotenv cors") + console.log(" 2. Add middleware (see references/integration.md)") + console.log(" 3. Test with: curl http://localhost:3000/api/private") + console.log("") +} + +main().catch((e) => { + console.error(`\n Bootstrap failed: ${e.message}\n`) + process.exit(1) +}) diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/package.json b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/package.json new file mode 100644 index 0000000000..331155a218 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/package.json @@ -0,0 +1,13 @@ +{ + "name": "express-oauth2-jwt-bearer-bootstrap", + "version": "1.0.0", + "description": "Bootstrap Auth0 configuration for Node OAuth2 JWT Bearer projects", + "type": "module", + "scripts": { + "auth0:bootstrap": "node bootstrap.mjs" + }, + "dependencies": { + "execa": "^9.0.0", + "ora": "^8.0.0" + } +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/apis.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/apis.mjs new file mode 100644 index 0000000000..e87f6e3444 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/apis.mjs @@ -0,0 +1,35 @@ +import { $ } from "execa" +import ora from "ora" +import { ChangeAction, createChangeItem } from "./change-plan.mjs" + +export function checkApiChanges(domain, apiConfig) { + const { framework } = apiConfig + const identifier = `https://${domain}/api/${framework}` + + return createChangeItem(ChangeAction.CREATE, { + resource: "API", + name: `${framework}-api`, + identifier, + }) +} + +export async function applyApiChanges(changePlan) { + const spinner = ora(`Creating API: ${changePlan.name}`).start() + try { + const createArgs = [ + "apis", "create", + "--name", changePlan.name, + "--identifier", changePlan.identifier, + "--signing-alg", "RS256", + "--json", + "--no-input", + ] + const { stdout } = await $({ timeout: 30000 })`auth0 ${createArgs}` + const api = JSON.parse(stdout) + spinner.succeed(`Created API: ${changePlan.name} (${api.identifier})`) + return api + } catch (e) { + spinner.fail("Failed to create API") + throw e + } +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/auth0-api.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/auth0-api.mjs new file mode 100644 index 0000000000..7784d93494 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/auth0-api.mjs @@ -0,0 +1,24 @@ +import { $ } from "execa" + +/** + * Make a generic API call using auth0 CLI. + */ +export async function auth0ApiCall(method, endpoint, data = null) { + const args = ["api", method, endpoint, "--no-input"] + + if (data) { + args.push("--data", JSON.stringify(data)) + } + + try { + const { stdout } = await $({ timeout: 30000 })`auth0 ${args}` + return stdout ? JSON.parse(stdout) : null + } catch (e) { + if (e.timedOut) { + console.warn(`Warning: API call timed out: auth0 api ${method} ${endpoint}`) + } else { + console.warn(`Warning: API call failed: ${e.message}`) + } + throw e + } +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/change-plan.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/change-plan.mjs new file mode 100644 index 0000000000..31a747c108 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/change-plan.mjs @@ -0,0 +1,12 @@ +export const ChangeAction = { + CREATE: "create", + UPDATE: "update", + SKIP: "skip", +} + +export function createChangeItem(action, details = {}) { + return { + action, + ...details, + } +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/discovery.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/discovery.mjs new file mode 100644 index 0000000000..078bb52805 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/discovery.mjs @@ -0,0 +1,48 @@ +import ora from "ora" +import { auth0ApiCall } from "./auth0-api.mjs" +import { ChangeAction } from "./change-plan.mjs" +import { checkApiChanges } from "./apis.mjs" + +export async function discoverExistingApis() { + const spinner = ora("Discovering existing APIs").start() + try { + const apis = (await auth0ApiCall("get", "resource-servers")) || [] + spinner.succeed("Discovered existing APIs") + return apis + } catch (e) { + const msg = e.message || String(e) + if (msg.includes("404") || msg.includes("Not Found")) { + spinner.succeed("No existing APIs found") + return [] + } + spinner.fail("Failed to discover APIs") + throw e + } +} + +export function buildChangePlan(apis, domain, apiConfig) { + const apiPlan = checkApiChanges(domain, apiConfig) + return { api: apiPlan } +} + +export function displayChangePlan(plan) { + console.log("\n Change Plan:\n") + + const items = [{ name: "API", ...plan.api }] + + for (const item of items) { + const icon = + item.action === ChangeAction.CREATE ? "+" : + item.action === ChangeAction.UPDATE ? "~" : "=" + const label = + item.action === ChangeAction.CREATE ? "CREATE" : + item.action === ChangeAction.UPDATE ? "UPDATE" : "SKIP " + + let detail = "" + if (item.identifier) detail = ` (identifier: ${item.identifier})` + + console.log(` ${icon} [${label}] ${item.name || item.resource}${detail}`) + } + + console.log("") +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/env-writer.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/env-writer.mjs new file mode 100644 index 0000000000..049c96f70c --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/env-writer.mjs @@ -0,0 +1,28 @@ +import fs from "node:fs" +import ora from "ora" + +export async function writeEnvFile(config, envFilePath) { + const spinner = ora("Writing .env").start() + + try { + let content = "" + if (fs.existsSync(envFilePath)) { + content = fs.readFileSync(envFilePath, "utf-8") + } + + for (const [key, value] of Object.entries(config)) { + const pattern = new RegExp(`^${key}=.*$`, "m") + if (pattern.test(content)) { + content = content.replace(pattern, `${key}=${value}`) + } else { + content += (content && !content.endsWith("\n") ? "\n" : "") + `${key}=${value}\n` + } + } + + fs.writeFileSync(envFilePath, content) + spinner.succeed(`Updated ${envFilePath}`) + } catch (e) { + spinner.fail("Failed to write .env") + throw e + } +} diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/helpers.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/helpers.mjs new file mode 100644 index 0000000000..786a467fb4 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/helpers.mjs @@ -0,0 +1,26 @@ +import readline from "node:readline/promises" + +export async function confirmWithUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} (y/N): `) + rl.close() + + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes" +} + +export async function getInputFromUser(message) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question(`${message} `) + rl.close() + + return answer.trim() +} + diff --git a/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/validation.mjs b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/validation.mjs new file mode 100644 index 0000000000..cb46bc8554 --- /dev/null +++ b/main/.mintlify/skills/express-oauth2-jwt-bearer/scripts/utils/validation.mjs @@ -0,0 +1,91 @@ +import { $ } from "execa" +import fs from "node:fs" +import path from "node:path" +import ora from "ora" + +export function checkNodeVersion() { + const [major] = process.versions.node.split(".").map(Number) + if (major < 20) { + console.error(`Node.js 20 or later is required (current: ${process.version})`) + process.exit(1) + } +} + +export async function checkAuth0CLI() { + const spinner = ora("Checking Auth0 CLI").start() + try { + const versionArgs = ["--version", "--no-input"] + const { stdout } = await $({ timeout: 10000 })`auth0 ${versionArgs}` + spinner.succeed(`Auth0 CLI found: ${stdout.trim()}`) + } catch { + spinner.fail("Auth0 CLI is not installed") + console.error( + "\nInstall it:\n" + + " macOS: brew install auth0/auth0-cli/auth0\n" + + " Linux: curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh\n" + + " More: https://github.com/auth0/auth0-cli\n" + ) + process.exit(1) + } +} + +export async function getActiveTenant() { + const spinner = ora("Detecting active tenant").start() + try { + const tenantsArgs = ["tenants", "list", "--csv", "--no-input"] + const { stdout } = await $({ timeout: 10000 })`auth0 ${tenantsArgs}` + + const activeLine = stdout + .split("\n") + .slice(1) + .find((line) => line.includes("\u2192")) + + const domain = activeLine?.split(",")[1]?.trim() + if (!domain) { + spinner.fail("No active tenant. Run `auth0 login` then re-run this script.") + process.exit(1) + } + + spinner.succeed(`Active tenant: ${domain}`) + return domain + } catch { + spinner.fail("Not logged in. Run `auth0 login` then re-run this script.") + process.exit(1) + } +} + +export function validateApiProject(projectPath) { + const spinner = ora("Validating API project").start() + + // Detect framework from project files + const detectors = [ + { file: "*.csproj", framework: "dotnet", port: 5000 }, + { file: "composer.json", framework: "laravel", port: 8000 }, + { file: "Gemfile", framework: "rails", port: 3000 }, + { file: "go.mod", framework: "go", port: 3000 }, + { file: "requirements.txt", framework: "python", port: 8000 }, + { file: "pyproject.toml", framework: "python", port: 8000 }, + { file: "package.json", framework: "node", port: 3000 }, + ] + + let framework = null + let port = 3000 + for (const d of detectors) { + const pattern = d.file.includes("*") + ? fs.readdirSync(projectPath).some((f) => f.endsWith(d.file.replace("*", ""))) + : fs.existsSync(path.join(projectPath, d.file)) + if (pattern) { + framework = d.framework + port = d.port + break + } + } + + if (!framework) { + spinner.fail(`Could not detect project framework in ${projectPath}`) + process.exit(1) + } + + spinner.succeed(`API project: ${framework} (port ${port})`) + return { framework, port } +}