diff --git a/apps/auth0-evals/src/evals/dpop/spa-js/PROMPT.md b/apps/auth0-evals/src/evals/dpop/spa-js/PROMPT.md new file mode 100644 index 0000000..ff26c62 --- /dev/null +++ b/apps/auth0-evals/src/evals/dpop/spa-js/PROMPT.md @@ -0,0 +1,16 @@ +--- +id: spa_js_dpop +name: SPA JS DPoP Token Binding +scaffold: src/evals/scaffolds/spa-js/auth0 +skills: auth0-dpop +setup_command: npm install +compile_command: npm run build +--- + +## Task + +My vanilla JavaScript SPA uses @auth0/auth0-spa-js for authentication. I want to add DPoP token binding so my API calls are protected against token replay attacks. + +Domain: dev-barkbook.us.auth0.com +Client ID: barkbook_client_abc123xyz +Audience: https://api.barkbook.com diff --git a/apps/auth0-evals/src/evals/dpop/spa-js/graders.ts b/apps/auth0-evals/src/evals/dpop/spa-js/graders.ts new file mode 100644 index 0000000..eb7722b --- /dev/null +++ b/apps/auth0-evals/src/evals/dpop/spa-js/graders.ts @@ -0,0 +1,51 @@ +import { contains, notContains, matches, judge, compiles, GraderLevel } from '@a0/eval-graders'; + +export function defineGraders() { + return [ + // ── L1: Positive presence ────────────────────────────────────────── + contains('@auth0/auth0-spa-js', 'Uses @auth0/auth0-spa-js SDK', GraderLevel.L1), + contains('useDpop', 'Enables useDpop option on Auth0 client', GraderLevel.L1), + contains('createFetcher', 'Uses createFetcher to get a DPoP-aware fetcher', GraderLevel.L1), + contains('fetchWithAuth', 'Uses fetchWithAuth for DPoP-protected API calls', GraderLevel.L1), + + // ── L2: Hallucination / wrong SDK ───────────────────────────────── + notContains('@auth0/auth0-react', 'No React SDK in vanilla JS app', GraderLevel.L2), + notContains('client_secret', 'No client_secret in SPA (public client)', GraderLevel.L2), + notContains('crypto.subtle', 'No manual DPoP key generation — SDK manages the key pair internally', GraderLevel.L2), + + // ── L3: Security checks ─────────────────────────────────────────── + notContains('localStorage.setItem', 'No tokens manually stored in localStorage', GraderLevel.L3), + notContains('sessionStorage.setItem', 'No tokens manually stored in sessionStorage', GraderLevel.L3), + + // ── L4: Structural / behavioral correctness ─────────────────────── + compiles('Project compiles with DPoP configuration', GraderLevel.L4), + matches(String.raw`useDpop\s*:\s*true`, 'useDpop: true set on Auth0 client', GraderLevel.L4), + matches(String.raw`createFetcher\s*\(`, 'createFetcher called on Auth0 client', GraderLevel.L4), + judge( + 'Does the code use auth0Client.createFetcher() and fetchWithAuth for the API request? ' + + 'When useDpop is enabled, fetchWithAuth automatically sends Authorization: DPoP ' + + 'plus a DPoP proof header. A manual fetch using only getTokenSilently() with ' + + 'Authorization: Bearer would lack the DPoP proof and be rejected by the server.', + GraderLevel.L4, + ), + + // ── L5: Version-specific API correctness ────────────────────────── + contains( + 'authorizationParams', + 'Uses authorizationParams (current v2 API, not deprecated top-level options)', + GraderLevel.L5, + ), + judge( + 'Does the solution use the SDK-provided DPoP fetcher (createFetcher / fetchWithAuth) ' + + 'rather than manually constructing DPoP proofs using WebCrypto, jose, or any third-party library?', + GraderLevel.L5, + ), + + // ── Holistic judge ──────────────────────────────────────────────── + judge( + 'Does the solution correctly add DPoP token binding to the vanilla JavaScript SPA using @auth0/auth0-spa-js, ' + + 'with useDpop: true on the Auth0 client and auth0Client.createFetcher() plus fetchWithAuth ' + + 'to automatically send Authorization: DPoP and the DPoP proof header on API calls?', + ), + ]; +} diff --git a/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/index.html b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/index.html new file mode 100644 index 0000000..6512f33 --- /dev/null +++ b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/index.html @@ -0,0 +1,24 @@ + + + + + + My App + + +

My App

+ +
+ +
+ + + + + + diff --git a/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/package.json b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/package.json new file mode 100644 index 0000000..54ddb1a --- /dev/null +++ b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/package.json @@ -0,0 +1,17 @@ +{ + "name": "spa-js-app", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@auth0/auth0-spa-js": "^2.0.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/src/app.js b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/src/app.js new file mode 100644 index 0000000..6c28575 --- /dev/null +++ b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/src/app.js @@ -0,0 +1,80 @@ +import { createAuth0Client } from '@auth0/auth0-spa-js'; + +const domain = 'dev-barkbook.us.auth0.com'; +const clientId = 'barkbook_client_abc123xyz'; +const audience = 'https://api.barkbook.com'; + +let auth0Client = null; + +async function initAuth0() { + auth0Client = await createAuth0Client({ + domain, + clientId, + authorizationParams: { + audience, + }, + }); + + const query = window.location.search; + if (query.includes('code=') && query.includes('state=')) { + await auth0Client.handleRedirectCallback(); + window.history.replaceState({}, document.title, window.location.pathname); + } + + await updateUI(); +} + +async function updateUI() { + const isAuthenticated = await auth0Client.isAuthenticated(); + + const loginSection = document.getElementById('login-section'); + const profileSection = document.getElementById('profile-section'); + + if (isAuthenticated) { + const user = await auth0Client.getUser(); + document.getElementById('user-name').textContent = `Name: ${user.name}`; + document.getElementById('user-email').textContent = `Email: ${user.email}`; + loginSection.style.display = 'none'; + profileSection.style.display = 'block'; + } else { + loginSection.style.display = 'block'; + profileSection.style.display = 'none'; + } +} + +async function fetchApiData() { + const accessToken = await auth0Client.getTokenSilently({ + authorizationParams: { audience }, + }); + + const response = await fetch('https://api.barkbook.com/data', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +document.getElementById('login-btn').addEventListener('click', async () => { + await auth0Client.loginWithRedirect({ + authorizationParams: { redirect_uri: window.location.origin }, + }); +}); + +document.getElementById('logout-btn').addEventListener('click', () => { + auth0Client.logout({ logoutParams: { returnTo: window.location.origin } }); +}); + +document.getElementById('call-api-btn').addEventListener('click', async () => { + try { + const data = await fetchApiData(); + console.log('API response:', data); + } catch (err) { + console.error('API call failed:', err); + } +}); + +initAuth0(); diff --git a/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/vite.config.js b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/vite.config.js new file mode 100644 index 0000000..8e3f14b --- /dev/null +++ b/apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + target: 'esnext', + }, +});