Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/auth0-evals/src/evals/dpop/spa-js/PROMPT.md
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions apps/auth0-evals/src/evals/dpop/spa-js/graders.ts
Original file line number Diff line number Diff line change
@@ -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 <token> ' +
'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?',
),
];
}
24 changes: 24 additions & 0 deletions apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<h1>My App</h1>

<div id="login-section">
<button id="login-btn">Log In</button>
</div>

<div id="profile-section" style="display: none">
<p id="user-name"></p>
<p id="user-email"></p>
<button id="logout-btn">Log Out</button>
<button id="call-api-btn">Call API</button>
</div>

<script type="module" src="/src/app.js"></script>
</body>
</html>
17 changes: 17 additions & 0 deletions apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
80 changes: 80 additions & 0 deletions apps/auth0-evals/src/evals/scaffolds/spa-js/auth0/src/app.js
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';

export default defineConfig({
build: {
target: 'esnext',
},
});
Loading