Complete JWT authentication with automatic header injection - zero manual header management required.
Authorization: Bearer ${token}<pan-auth> on the page, done<!DOCTYPE html>
<meta charset="utf-8">
<script type="module" src="./pan/core/pan-autoload.mjs"></script>
<!-- 1. Auth Manager -->
<pan-auth storage="localStorage" auto-refresh="true"></pan-auth>
<!-- 2. Data Connector (auto-adds auth headers!) -->
<pan-data-connector resource="users" base-url="/api"></pan-data-connector>
<!-- 3. Your components (don't know about auth!) -->
<user-list></user-list>
<script type="module">
import { PanClient } from './pan/core/pan-client.mjs';
const pc = new PanClient();
// Login - that's it!
await pc.request('auth.login', {
email: 'user@example.com',
password: 'password123'
});
// Now all requests automatically include auth header
pc.publish({ topic: 'users.list.get', data: {} });
</script>
βββββββββββββββββββ
β <pan-auth> β β Manages JWT tokens, login/logout, auto-refresh
β Auth Manager β Publishes: auth.state (retained)
ββββββββββ¬βββββββββ
β
βΌ (listens to auth.internal.state)
βββββββββββββββββββ
β<pan-connector> β β Auto-injects Authorization: Bearer ${token}
β Data Connector β Works with ANY HTTP request
βββββββββββββββββββ
<pan-auth> manages token lifecycle
auth.internal.state with token (for connectors)auth.state without token (for UI components)pan-data-connector, pan-graphql-connector)
auth.internal.stateAuthorization header if token exists<pan-auth> - Authentication ManagerManages JWT authentication state via PAN message bus.
<pan-auth
storage="localStorage" <!-- localStorage | sessionStorage | memory -->
token-key="pan_jwt" <!-- Key for storing token -->
refresh-key="pan_refresh_jwt" <!-- Key for storing refresh token -->
auto-refresh="true" <!-- Enable automatic token refresh -->
refresh-before="300" <!-- Refresh N seconds before expiry -->
login-endpoint="/api/auth/login" <!-- Login API endpoint -->
refresh-endpoint="/api/auth/refresh" <!-- Refresh API endpoint -->
logout-endpoint="/api/auth/logout"> <!-- Logout API endpoint -->
</pan-auth>
Commands (publish these):
// Login
await pc.request('auth.login', {
email: 'user@example.com',
password: 'password123'
});
// Response: { ok: true, user: {...} }
// Logout
pc.publish({ topic: 'auth.logout', data: {} });
// Refresh token (usually automatic)
pc.publish({ topic: 'auth.refresh', data: {} });
// Set token directly (external login)
pc.publish({
topic: 'auth.setToken',
data: {
token: 'eyJhbGc...',
refreshToken: 'refresh...',
user: { id: 1, email: 'user@example.com' }
}
});
// Check current auth state
pc.publish({ topic: 'auth.check', data: {} });
Events (subscribe to these):
// Auth state changed (retained)
pc.subscribe('auth.state', msg => {
console.log('Authenticated:', msg.data.authenticated);
console.log('User:', msg.data.user);
console.log('Expires at:', msg.data.expiresAt);
}, { retained: true });
// Login events
pc.subscribe('auth.login.success', msg => {
console.log('Login successful:', msg.data.user);
});
pc.subscribe('auth.login.error', msg => {
console.error('Login failed:', msg.data.error);
});
// Logout event
pc.subscribe('auth.logout.success', () => {
console.log('Logged out');
});
// Refresh events
pc.subscribe('auth.refresh.success', () => {
console.log('Token refreshed');
});
pc.subscribe('auth.refresh.error', msg => {
console.error('Refresh failed:', msg.data.error);
});
Published State:
// auth.state (public - for UI components)
{
authenticated: true,
user: {
id: 1,
email: 'user@example.com',
name: 'John Doe',
role: 'admin'
},
expiresAt: 1234567890000,
hasRefreshToken: true
}
// auth.internal.state (internal - for connectors)
{
authenticated: true,
token: 'eyJhbGc...', // <-- Includes actual token!
refreshToken: 'refresh...',
user: {...},
expiresAt: 1234567890000
}
All connectors automatically inject auth headers when available.
<pan-data-connector> (REST)<pan-data-connector
resource="users"
base-url="/api">
</pan-data-connector>
<script type="module">
import { PanClient } from './pan/core/pan-client.mjs';
const pc = new PanClient();
// This request automatically includes:
// Authorization: Bearer eyJhbGc...
pc.publish({ topic: 'users.list.get', data: {} });
</script>
<pan-graphql-connector><pan-graphql-connector
resource="users"
endpoint="/api/graphql">
<script type="application/graphql" data-op="list">
query GetUsers {
users { id name email role }
}
</script>
</pan-graphql-connector>
<script type="module">
import { PanClient } from './pan/core/pan-client.mjs';
const pc = new PanClient();
// GraphQL request automatically includes:
// Authorization: Bearer eyJhbGc...
pc.publish({ topic: 'users.list.get', data: {} });
</script>
panFetch - Authenticated Fetch UtilityFor custom API calls, use panFetch - a drop-in replacement for fetch() that auto-injects auth headers.
import { panFetch } from './pan/core/pan-fetch.mjs';
// Automatically includes Authorization header if logged in
const response = await panFetch.fetch('/api/users');
const data = await response.json();
// Convenience methods
const users = await panFetch.get('/api/users');
const newUser = await panFetch.post('/api/users', { name: 'John' });
const updated = await panFetch.put('/api/users/1', { name: 'Jane' });
await panFetch.delete('/api/users/1');
// Check auth status
if (panFetch.isAuthenticated()) {
console.log('User is logged in');
}
API:
panFetch.fetch(url, options) // Drop-in replacement for fetch()
panFetch.fetchJson(url, options) // Fetch + auto JSON parse + error handling
panFetch.get(url, options) // GET with JSON
panFetch.post(url, body, options) // POST with JSON
panFetch.put(url, body, options) // PUT with JSON
panFetch.patch(url, body, options) // PATCH with JSON
panFetch.delete(url, options) // DELETE with JSON
panFetch.isAuthenticated() // Check if logged in
panFetch.getAuthState() // Get current auth state
customElements.define('login-form', class extends HTMLElement {
connectedCallback() {
this.pc = new PanClient(this);
this.innerHTML = `
<form id="login">
<input name="email" type="email" required>
<input name="password" type="password" required>
<button>Login</button>
</form>
`;
this.querySelector('#login').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await this.pc.request('auth.login', {
email: formData.get('email'),
password: formData.get('password')
});
if (response.data.ok) {
console.log('Logged in!', response.data.user);
}
} catch (error) {
console.error('Login failed:', error);
}
});
// Listen for login events
this.pc.subscribe('auth.login.success', msg => {
alert(`Welcome, ${msg.data.user.name}!`);
});
this.pc.subscribe('auth.login.error', msg => {
alert(`Login failed: ${msg.data.error}`);
});
}
});
customElements.define('user-list', class extends HTMLElement {
connectedCallback() {
this.pc = new PanClient(this);
// Subscribe to auth state
this.pc.subscribe('auth.state', msg => {
if (msg.data.authenticated) {
// User logged in - fetch data
// Auth header automatically injected by connector!
this.pc.publish({ topic: 'users.list.get', data: {} });
} else {
// User logged out - show login prompt
this.innerHTML = '<p>Please log in to view users.</p>';
}
}, { retained: true });
// Subscribe to users data
this.pc.subscribe('users.list.state', msg => {
this.renderUsers(msg.data.items);
}, { retained: true });
}
renderUsers(users) {
this.innerHTML = `
<ul>
${users.map(u => `<li>${u.name} (${u.email})</li>`).join('')}
</ul>
`;
}
});
<pan-auth> automatically refreshes tokens before they expire:
<pan-auth
auto-refresh="true"
refresh-before="300"> <!-- Refresh 5 minutes before expiry -->
</pan-auth>
How it works:
pan-auth decodes JWT to get expiration timerefresh-endpoint with current tokenauth.state// Manually trigger refresh
pc.publish({ topic: 'auth.refresh', data: {} });
// Listen for refresh events
pc.subscribe('auth.refresh.success', () => {
console.log('Token refreshed successfully');
});
pc.subscribe('auth.refresh.error', msg => {
console.error('Refresh failed:', msg.data.error);
// Token expired - redirect to login
window.location.href = '/login';
});
Tokens persist across browser restarts.
<pan-auth storage="localStorage"></pan-auth>
Tokens cleared when browser tab closes.
<pan-auth storage="sessionStorage"></pan-auth>
Tokens only in memory - cleared on page refresh.
<pan-auth storage="memory"></pan-auth>
POST /api/auth/loginRequest:
{
"email": "user@example.com",
"password": "password123"
}
Response (200 OK):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "refresh_token_here",
"user": {
"id": 1,
"email": "user@example.com",
"name": "John Doe",
"role": "admin"
}
}
Response (401 Unauthorized):
{
"error": "Invalid email or password"
}
POST /api/auth/refreshRequest Headers:
Authorization: Bearer refresh_token_here
Request Body:
{
"refreshToken": "refresh_token_here"
}
Response (200 OK):
{
"token": "new_access_token",
"refreshToken": "new_refresh_token",
"user": {
"id": 1,
"email": "user@example.com",
"name": "John Doe",
"role": "admin"
}
}
POST /api/auth/logoutRequest Headers:
Authorization: Bearer access_token_here
Response (200 OK):
{
"ok": true
}
All requests automatically include Authorization header:
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Backend should:
Authorization: Bearer <token>localStorage for persistent sessionssessionStorage for temporary sessionsmemory for maximum security (no persistence)Backend must:
auth.state is public - safe for UIauth.internal.state includes token - only for connectors/examples/auth-demo.htmlComplete authentication demo with:
# Open in browser
open examples/auth-demo.html
/examples/mock-auth-api.mjsClient-side JWT simulator for testing:
/api/* requestsTest Credentials:
admin@example.com / admin123user@example.com / user123demo@example.com / demoBefore:
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}` // Manual!
}
});
After:
// Just use the connector - header automatically added!
pc.publish({ topic: 'users.list.get', data: {} });
// Or use panFetch for custom calls
const response = await panFetch.fetch('/api/users');
If you have an existing auth system:
// After login with your system, set the token
pc.publish({
topic: 'auth.setToken',
data: {
token: yourAccessToken,
refreshToken: yourRefreshToken,
user: { id: 1, email: 'user@example.com', name: 'John' }
}
});
// Now all PAN requests include the token automatically
Check:
<pan-auth> on the page?auth.state<pan-auth> in HTML?// Debug auth state
pc.subscribe('auth.state', msg => {
console.log('Auth State:', msg.data);
}, { retained: true });
pc.publish({ topic: 'auth.check', data: {} });
Check:
// Check token expiration
pc.subscribe('auth.state', msg => {
const exp = new Date(msg.data.expiresAt);
console.log('Token expires:', exp);
console.log('Time until expiry:', Math.floor((msg.data.expiresAt - Date.now()) / 1000), 'seconds');
}, { retained: true });
Check:
auto-refresh="true" on <pan-auth>?refresh-endpoint correct?// Monitor refresh events
pc.subscribe('auth.refresh.success', () => {
console.log('β Token refreshed');
});
pc.subscribe('auth.refresh.error', msg => {
console.error('β Refresh failed:', msg.data.error);
});
Q: Do I need to import anything in my components? A: No! Components just publish/subscribe topics. Auth is completely transparent.
Q: What if I need a different auth header format?
A: Connectors use Authorization: Bearer ${token} by default. For custom formats, modify the connector or use panFetch with custom headers.
Q: Can I use this with OAuth/SSO?
A: Yes! After OAuth callback, use auth.setToken to inject the token into PAN.
Q: Does this work with GraphQL?
A: Yes! <pan-graphql-connector> auto-injects headers just like REST connector.
Q: How do I handle token expiration?
A: Enable auto-refresh="true". Tokens refresh automatically before expiry.
Q: Can I test without a backend?
A: Yes! Use /examples/mock-auth-api.mjs - a client-side JWT simulator.
Zero manual header management:
<pan-auth> on pagepanFetchBenefits:
Authorization: Bearer ${token}Philosophy:
Auth should be invisible infrastructure, not something developers think about on every request.