
Auth0 Spa Js Skill
- 437 installs
- 39 repo stars
- auth0/agent-skills
Authenticate single-page applications with Auth0 SDK for JavaScript and secure API calls.
About
Auth0 SPA JS skill integrates Auth0 into JavaScript single-page applications. Frontend developers use it to add secure authentication with PKCE flow and automatic token management.
- Auth0 SPA SDK
- PKCE flow
- Token refresh
Auth0 Spa Js by the numbers
- 437 all-time installs (skills.sh)
- Ranked #658 of 2,307 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/auth0/agent-skills --skill auth0-spa-jsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 437 |
|---|---|
| repo stars | ★ 39 |
| Repository | auth0/agent-skills ↗ |
What it does
Authenticate single-page applications with Auth0 SDK for JavaScript and secure API calls.
Files
Auth0 SPA JS Integration
Add authentication to any browser-based single-page application using @auth0/auth0-spa-js — the low-level Auth0 SDK for Vanilla JS, Svelte, SolidJS, and any SPA not using React, Angular, or Vue.
Agent instruction: Before providing SDK setup instructions, fetch the latest release version by running:
```
gh api repos/auth0/auth0-spa-js/releases/latest --jq '.tag_name'
```
Use the returned version in all dependency lines instead of any hardcoded version below. If the command fails, fall back to checking https://github.com/auth0/auth0-spa-js/releases.
Prerequisites
- Modern browser with ES2017+ support
- npm or yarn (or use CDN for no-bundler apps)
- Auth0 account with a Single Page Application configured
- If you don't have Auth0 set up, see auth0-quickstart
When NOT to Use
- React SPAs (hooks, Auth0Provider): Use auth0-react
- Angular SPAs (AuthModule, AuthGuard): Use auth0-angular
- Vue 3 SPAs (createAuth0, useAuth0): Use auth0-vue
- Next.js (server-side sessions, App Router): Use auth0-nextjs
- Nuxt (server-side SSR): Use auth0-nuxt
- Express server-side web apps: Use auth0-express
- React Native / Expo mobile apps: Use auth0-react-native
Quick Start Workflow
1. Install SDK
npm install @auth0/auth0-spa-jsOr via CDN (no bundler). Run this to get the latest version, then use it in your HTML:
VERSION=$(npm view @auth0/auth0-spa-js version)<script src="https://cdn.auth0.com/js/auth0-spa-js/$VERSION/auth0-spa-js.production.js"></script>2. Configure Auth0
For automated setup with Auth0 CLI, see Setup Guide for complete scripts.
For manual setup, create .env (Vite):
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-idIn Auth0 Dashboard, set for your Single Page Application:
- Allowed Callback URLs:
http://localhost:5173 - Allowed Logout URLs:
http://localhost:5173 - Allowed Web Origins:
http://localhost:5173
3. Initialize Auth0 Client
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
}
});
// Handle redirect callback after login
const query = new URLSearchParams(window.location.search);
if ((query.has('code') || query.has('error')) && query.has('state')) {
await auth0.handleRedirectCallback();
window.history.replaceState({}, document.title, window.location.pathname);
}4. Add Login / Logout
Agent instruction: Before adding new UI elements, search the project for existing click handlers for login, logout, sign-in, or sign-out buttons. If existing handlers are found, hook the Auth0 code into them without modifying the existing UI. Only create new buttons if no existing handlers are found.
// Login
document.getElementById('login-btn').addEventListener('click', async () => {
await auth0.loginWithRedirect();
});
// Logout
document.getElementById('logout-btn').addEventListener('click', () => {
auth0.logout({
logoutParams: { returnTo: window.location.origin }
});
});
// Update UI based on auth state
const isAuthenticated = await auth0.isAuthenticated();
if (isAuthenticated) {
const user = await auth0.getUser();
console.log(user.name, user.email);
}5. Get Access Tokens for API Calls
const accessToken = await auth0.getTokenSilently();
const response = await fetch('https://your-api.example.com/data', {
headers: { Authorization: `Bearer ${accessToken}` }
});6. Build & Verify
Agent instruction: After completing the integration, build the project to verify it compiles successfully:
```bash
npm run build
```
If the build fails, analyze the error output and fix the issues. Common integration build failures include:
- Module not found: Missing npm install @auth0/auth0-spa-js — run the install command- Cannot find name 'import.meta': TypeScript target too low — set"target": "ES2020"or higher intsconfig.json
- `createAuth0Client` is not a function: Wrong import path or CDN usage without bundle step
- Env vars undefined at runtime: Vite requiresVITE_prefix; webpack/CRA requiresREACT_APP_prefix
>
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
Detailed Documentation
- **Setup Guide** — Automated setup scripts (Bash/PowerShell), Auth0 CLI commands,
.envconfiguration, callback URL setup - **Integration Patterns** — Token management, calling APIs, refresh tokens, organizations, MFA, DPoP, error handling, advanced patterns
- **Testing & Reference** — Configuration options, claims reference, testing checklist, common issues, security considerations
Common Mistakes
| Mistake | Fix |
|---|---|
Callback URL port mismatch (e.g., localhost:3001 vs localhost:5173) | Match Allowed Callback URLs exactly to your dev server port in Auth0 Dashboard |
client_secret in SPA code | SPAs must never have a client secret — remove it. Auth0 sets auth method to None for SPA apps |
Tokens stored in localStorage | Use in-memory storage (default) or sessionStorage. Never localStorage — XSS risk |
getTokenSilently() throws login_required on page refresh | Add your app origin to Allowed Web Origins in Auth0 Dashboard |
handleRedirectCallback() not called after redirect | Must call after login redirect to exchange the auth code; without this the URL params persist and re-trigger |
Domain includes https:// prefix | Auth0 domain should be hostname only: your-tenant.auth0.com, not https://your-tenant.auth0.com |
loginWithPopup() called from async init code | Popups must be triggered directly from a user gesture (click handler). Never call from init or page load code |
Using Auth0Provider from @auth0/auth0-react in Vanilla JS | For Vanilla JS, use createAuth0Client() directly — no provider component needed |
Related Skills
- auth0-quickstart — Set up an Auth0 account and application
- auth0-react — Auth0 for React SPAs with hooks
- auth0-angular — Auth0 for Angular SPAs
- auth0-vue — Auth0 for Vue 3 SPAs
- auth0-mfa — Add Multi-Factor Authentication
- auth0-cli — Manage Auth0 resources from the terminal
Quick Reference
Core Methods
| Method | Description |
|---|---|
createAuth0Client(options) | Create and initialize client (calls checkSession internally) |
new Auth0Client(options) | Instantiate without auto session check |
auth0.loginWithRedirect(options?) | Redirect to Auth0 Universal Login |
auth0.loginWithPopup(options?) | Open Auth0 login in a popup |
auth0.logout(options?) | Clear session and redirect |
auth0.handleRedirectCallback(url?) | Process redirect result after login |
auth0.isAuthenticated() | Promise<boolean> |
auth0.getUser() | `Promise<User \ |
auth0.getTokenSilently(options?) | Promise<string> — access token |
auth0.checkSession() | Attempt silent re-authentication |
Common Use Cases
- Login/Logout → See Step 4 above
- Protecting content → Integration Guide
- API calls with tokens → Integration Guide
- Refresh tokens → Integration Guide
- Organizations → Integration Guide
- MFA handling → Integration Guide
- Error handling → Integration Guide
References
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 |
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' \ |
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:
import.meta.env.VITE_AUTH0_DOMAINCRA access:
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\ |
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()returnstrueafter successful login - [ ]
auth0.getUser()returns profile withsub,name,email - [ ] Logout clears session and redirects to
returnToURL - [ ] After logout,
isAuthenticated()returnsfalse
Token Management
- [ ]
getTokenSilently()returns a JWT string - [ ] Access token decoded at 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_requirederror ongetTokenSilentlytriggers re-authentication - [ ] Network failure in
getTokenSilentlyis 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_secretanywhere in source code or.env - [ ] Dev
.envfile 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.
Auth0 SPA JS Integration Patterns
---
Client Initialization
Using createAuth0Client (Recommended)
createAuth0Client initializes the client and automatically calls checkSession() to restore any existing session:
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:
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
// 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:
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):
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
// 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
// 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:
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
// 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
const { access_token, token_type, id_token, expires_in } = await auth0.getTokenSilently({
detailedResponse: true
});Token for a Specific Audience
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):
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
await auth0.loginWithRedirect({
authorizationParams: {
organization: 'org_xxxxxxxxxxxx' // or organization name
}
});Initialize Client with Organization
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
async function switchOrganization(orgId) {
await auth0.logout({ openUrl: false });
await auth0.loginWithRedirect({
authorizationParams: { organization: orgId }
});
}Accept User Invitations
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:
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:
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
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
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
// 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 })
})
}));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:
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
.envdoes not exist, ask: - Question: "This setup will create a
.envfile containing Auth0 credentials (domain and client ID). Do you want to proceed?" - Options: "Yes, create .env" / "No, I'll configure it manually"
- If
.envalready exists, ask: - Question: "A
.envfile 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)
#!/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)
# 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
npm install @auth0/auth0-spa-jsStep 2: Install Auth0 CLI
macOS:
brew install auth0/auth0-cli/auth0Linux:
curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | shWindows:
scoop install auth0Step 3: Get Credentials
# Login to Auth0
auth0 login
# List your apps
auth0 apps list
# Get app details (replace <app-id>)
auth0 apps show <app-id>Step 4: Create .env File
For Vite-based projects:
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-idFor Create React App:
REACT_APP_AUTH0_DOMAIN=your-tenant.auth0.com
REACT_APP_AUTH0_CLIENT_ID=your-client-idFor Webpack / plain HTML:
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your-client-id---
Creating an Auth0 Application via Dashboard
1. Go to Auth0 Dashboard 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
.envto.gitignore(to avoid accidental commits with other sensitive env vars) - Use
.env.localfor 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(notprocess.env)
Create React App:
- Ensure variables start with
REACT_APP_ - Restart dev server after changes
Auth0 CLI Issues
Browser doesn't open for login:
auth0 login --no-browser"Not logged in" error:
auth0 login --force---
Next Steps
After setup is complete: 1. Return to main skill guide for integration steps 2. See Integration Guide for advanced patterns 3. Check API Reference for complete SDK documentation
#!/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)
})
{
"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"
}
}
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
}
}
export const ChangeAction = {
CREATE: "create",
UPDATE: "update",
SKIP: "skip",
}
export function createChangeItem(action, details = {}) {
return {
action,
...details,
}
}
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
}
}
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
}
}
}
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("")
}
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
}
}
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
}
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 }
}
{
"skill_name": "auth0-spa-js",
"evals": [
{
"id": 1,
"prompt": "Add Auth0 authentication to a Vanilla JS application using the @auth0/auth0-spa-js SDK.\n\n**Auth0 Credentials:**\n- Domain: `dev-example.auth0.com`\n- Client ID: `abc123def456ghi789jkl012`",
"expected_output": "Working Vanilla JS SPA with Auth0 client initialization, login/logout, and user profile access",
"expectations": [
"Auth0 SPA JS SDK installed as an npm dependency",
"Has correct import statement for @auth0/auth0-spa-js",
"Auth0 client created (via createAuth0Client or Auth0Client constructor)",
"Has Auth0 client initialization",
"Implements login functionality",
"Implements logout functionality",
"Auth0 domain dev-example.auth0.com written to .env config file",
"Client ID abc123def456ghi789jkl012 written to .env config file",
"Does not use server-side SDK in SPA",
"Does not store tokens in localStorage",
"No client secret in SPA code",
"Correctly integrates Auth0 into a Vanilla JS SPA with login, logout, and user profile",
"Uses current SDK version (v2.18.3) in package.json dependency declaration",
"Configures refresh token rotation (useRefreshTokens: true)",
"Uses correct framework-specific env var prefix (VITE_AUTH0_ for Vite, REACT_APP_AUTH0_ for CRA)",
"Calls handleRedirectCallback() to process redirect after login",
"Does not use deprecated Auth0 Lock or legacy WebAuth",
"Uses skill-specific advanced patterns (version, refresh tokens, env var prefix, or callback handling)"
]
}
]
}
[
{"type": "matches", "pattern": "@auth0/auth0-spa-js|\"dependencies\"[\\s\\S]*?@auth0/auth0-spa-js", "description": "Auth0 SPA JS SDK installed as an npm dependency"},
{"type": "matches", "pattern": "import.*@auth0/auth0-spa-js|require.*@auth0/auth0-spa-js", "description": "Has correct import statement for @auth0/auth0-spa-js"},
{"type": "contains_any", "values": ["createAuth0Client(", "new Auth0Client("], "description": "Auth0 client created (via createAuth0Client or Auth0Client constructor)"},
{"type": "matches", "pattern": "createAuth0Client\\(|new Auth0Client\\(", "description": "Has Auth0 client initialization"},
{"type": "matches", "pattern": "loginWithRedirect\\(|loginWithPopup\\(", "description": "Implements login functionality"},
{"type": "matches", "pattern": "\\.logout\\(", "description": "Implements logout functionality"},
{"type": "file_contains", "file_pattern": "**/.env*", "value": "dev-example.auth0.com", "description": "Auth0 domain written to .env config file"},
{"type": "file_contains", "file_pattern": "**/.env*", "value": "abc123def456ghi789jkl012", "description": "Client ID written to .env config file"},
{"type": "not_contains_any", "values": ["@auth0/nextjs-auth0", "@auth0/express-openid-connect"], "description": "Does not use server-side SDK in SPA"},
{"type": "not_contains", "value": "localStorage", "description": "Does not store tokens in localStorage"},
{"type": "not_contains", "value": "client_secret", "description": "No client secret in SPA code"},
{"type": "judge", "question": "Does the solution correctly integrate Auth0 into a Vanilla JS or framework-agnostic SPA using @auth0/auth0-spa-js with working login, logout, and user profile access? The implementation should use createAuth0Client() or Auth0Client, call handleRedirectCallback() after redirect, and NOT store tokens in localStorage.", "description": "Correctly integrates Auth0 into a Vanilla JS SPA with login, logout, and user profile", "examples": "PASS: Uses createAuth0Client() with domain and clientId, calls handleRedirectCallback() on page load, implements loginWithRedirect() and logout() with logoutParams.returnTo.\nPASS: Uses Auth0Client constructor with authorizationParams, handles redirect callback, updates UI based on isAuthenticated().\nFAIL: Uses @auth0/auth0-react or Auth0Provider component (wrong SDK for Vanilla JS).\nFAIL: Stores access token in localStorage.\nFAIL: Missing handleRedirectCallback() call — URL params not cleaned up after login.", "framework": "vanilla-js"},
{"type": "matches", "pattern": "\\^2\\.18|~2\\.18|\"2\\.18\\.", "description": "Uses current SDK version (v2.18.3) in package.json dependency declaration"},
{"type": "matches", "pattern": "useRefreshTokens\\s*:\\s*true", "description": "Configures refresh token rotation (useRefreshTokens: true)"},
{"type": "matches", "pattern": "VITE_AUTH0_|REACT_APP_AUTH0_", "description": "Uses correct framework-specific env var prefix (VITE_AUTH0_ for Vite, REACT_APP_AUTH0_ for CRA)"},
{"type": "matches", "pattern": "handleRedirectCallback\\(", "description": "Calls handleRedirectCallback() to process redirect after login"},
{"type": "not_contains_any", "values": ["auth0.lock", "Auth0Lock", "WebAuth("], "description": "Does not use deprecated Auth0 Lock or legacy WebAuth (uses @auth0/auth0-spa-js instead)"},
{"type": "judge", "question": "Check ONLY for these skill-specific advanced patterns (NOT basic SDK usage like createAuth0Client, loginWithRedirect, logout, isAuthenticated, getUser). Look for at least 2 of: (1) SDK version 2.18.x in a package.json dependency (e.g., '^2.18', '~2.18', '2.18.3'), (2) useRefreshTokens: true in the client configuration, (3) correct framework-specific env var prefix (VITE_AUTH0_ for Vite projects or REACT_APP_AUTH0_ for CRA), (4) handleRedirectCallback() called with URL param check before calling it. These are advanced patterns taught by the skill that are NOT part of standard quickstart knowledge. Answer YES only if at least 2 of these advanced patterns are present, NO otherwise.", "description": "Uses skill-specific advanced patterns (version, refresh tokens, env var prefix, or callback handling)", "examples": "PASS: Code has '^2.18.3' in package.json AND configures useRefreshTokens: true.\nPASS: Code uses VITE_AUTH0_DOMAIN env var AND calls handleRedirectCallback() with a proper code/state check.\nFAIL: Code only uses createAuth0Client() with hardcoded domain and clientId and basic loginWithRedirect() — no version specified, no refresh tokens, no env var prefix.\nFAIL: Code has correct basic structure but stores token in localStorage and doesn't check for code/state before calling handleRedirectCallback.", "framework": "vanilla-js"}
]
Agent System
You are a software developer adding Auth0 authentication to a Vanilla JS single-page application. You have access to tools for reading/writing files, running commands, and fetching URLs. Use these tools to complete the integration task below.
Task
Add Auth0 authentication to a Vanilla JS application using the @auth0/auth0-spa-js SDK.
Auth0 Credentials:
- Domain:
dev-example.auth0.com - Client ID:
abc123def456ghi789jkl012