Full Auth System Guide

Build a production-ready authentication system with QAuth. This tutorial covers signup, login, sessions, protected routes, and token refresh.

1. Architecture Overview

A typical QAuth-powered system has three components: browser (with QAuthClient), API server (with QAuthValidator), and auth server (with QAuthServer).

QAuthClient

Browser

Creates proofs, stores tokens

QAuthValidator

API Server

Validates tokens & proofs

QAuthServer

Auth Server

Issues & refreshes tokens

PostgreSQL

Database

Users, sessions, tokens

2. Database Schema

Three tables: users, sessions (linked to token JTI), and refresh tokens with rotation support.

schema.sql
1-- Users table
2CREATE TABLE users (
3 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4 email VARCHAR(255) UNIQUE NOT NULL,
5 password_hash TEXT NOT NULL,
6 display_name VARCHAR(255),
7 roles TEXT[] DEFAULT '{user}',
8 mfa_enabled BOOLEAN DEFAULT false,
9 created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10 updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
11);
12
13-- Sessions table
14CREATE TABLE sessions (
15 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
16 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
17 token_jti VARCHAR(64) UNIQUE NOT NULL,
18 client_public_key TEXT,
19 ip_address VARCHAR(45),
20 user_agent TEXT,
21 expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
22 created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
23);
24
25-- Refresh tokens table
26CREATE TABLE refresh_tokens (
27 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
28 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
29 session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
30 token_hash VARCHAR(64) UNIQUE NOT NULL,
31 expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
32 revoked BOOLEAN DEFAULT false,
33 created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
34);
35
36CREATE INDEX idx_sessions_user_id ON sessions(user_id);
37CREATE INDEX idx_sessions_token_jti ON sessions(token_jti);
38CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
39CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);

3. Server Setup

Initialize QAuthServer once at startup. Share public keys with API servers.

lib/server.ts
1import { QAuthServer, PolicyEngine } from '@quantumshield/qauth';
2
3// Initialize QAuth server (do this once at startup)
4const qauth = new QAuthServer({
5 issuer: 'https://auth.yourapp.com',
6 audience: 'https://api.yourapp.com',
7});
8
9// Share these public keys with your API servers
10const publicKeys = qauth.getPublicKeys();
11
12// Initialize policy engine
13const policyEngine = new PolicyEngine();
14policyEngine.loadPolicy({
15 id: 'urn:qauth:policy:app',
16 version: '1.0',
17 issuer: 'https://auth.yourapp.com',
18 rules: [
19 {
20 id: 'authenticated-read',
21 effect: 'allow',
22 resources: ['api/*'],
23 actions: ['read', 'list'],
24 },
25 {
26 id: 'own-resources',
27 effect: 'allow',
28 resources: ['api/users/{userId}/**'],
29 actions: ['read', 'update'],
30 },
31 {
32 id: 'admin-full',
33 effect: 'allow',
34 resources: ['api/**'],
35 actions: ['*'],
36 conditions: { custom: { role: { in: ['admin'] } } },
37 priority: 10,
38 },
39 ],
40});
41
42export { qauth, policyEngine, publicKeys };

4. User Signup

Hash password with bcrypt, create user record, issue QAuth token, create session and refresh token.

lib/auth/signup.ts
1import { qauth } from './server';
2import bcrypt from 'bcrypt';
3import { db } from './database';
4
5export async function signup(email: string, password: string, name: string) {
6 // 1. Validate input
7 if (!email || !password || password.length < 8) {
8 throw new Error('Invalid input');
9 }
10
11 // 2. Check if user exists
12 const existing = await db.query(
13 'SELECT id FROM users WHERE email = $1', [email]
14 );
15 if (existing.rows.length > 0) {
16 throw new Error('Email already registered');
17 }
18
19 // 3. Hash password
20 const passwordHash = await bcrypt.hash(password, 12);
21
22 // 4. Create user
23 const result = await db.query(
24 `INSERT INTO users (email, password_hash, display_name)
25 VALUES ($1, $2, $3) RETURNING id, email, roles`,
26 [email, passwordHash, name]
27 );
28 const user = result.rows[0];
29
30 // 5. Create QAuth token
31 const token = qauth.createToken({
32 subject: user.id,
33 policyRef: 'urn:qauth:policy:app',
34 validitySeconds: 3600, // 1 hour
35 claims: {
36 email: user.email,
37 roles: user.roles,
38 },
39 });
40
41 // 6. Store session
42 const payload = qauth.validateToken(token);
43 await db.query(
44 `INSERT INTO sessions (user_id, token_jti, expires_at)
45 VALUES ($1, $2, to_timestamp($3))`,
46 [user.id, payload.jti, payload.exp]
47 );
48
49 // 7. Create refresh token
50 const refreshToken = crypto.randomUUID();
51 const refreshHash = await bcrypt.hash(refreshToken, 10);
52 await db.query(
53 `INSERT INTO refresh_tokens (user_id, session_id, token_hash, expires_at)
54 VALUES ($1, (SELECT id FROM sessions WHERE token_jti = $2),
55 $3, NOW() + INTERVAL '30 days')`,
56 [user.id, payload.jti, refreshHash]
57 );
58
59 return { token, refreshToken, user: { id: user.id, email, name } };
60}

5. User Login

Verify credentials, then issue a fresh token and refresh token. Same flow as signup minus user creation.

lib/auth/login.ts
1import { qauth } from './server';
2import bcrypt from 'bcrypt';
3import { db } from './database';
4
5export async function login(email: string, password: string) {
6 // 1. Find user
7 const result = await db.query(
8 'SELECT id, email, password_hash, roles, display_name FROM users WHERE email = $1',
9 [email]
10 );
11 if (result.rows.length === 0) {
12 throw new Error('Invalid credentials');
13 }
14 const user = result.rows[0];
15
16 // 2. Verify password
17 const valid = await bcrypt.compare(password, user.password_hash);
18 if (!valid) {
19 throw new Error('Invalid credentials');
20 }
21
22 // 3. Create QAuth token
23 const token = qauth.createToken({
24 subject: user.id,
25 policyRef: 'urn:qauth:policy:app',
26 validitySeconds: 3600,
27 claims: {
28 email: user.email,
29 roles: user.roles,
30 name: user.display_name,
31 },
32 });
33
34 // 4. Store session
35 const payload = qauth.validateToken(token);
36 await db.query(
37 `INSERT INTO sessions (user_id, token_jti, expires_at)
38 VALUES ($1, $2, to_timestamp($3))`,
39 [user.id, payload.jti, payload.exp]
40 );
41
42 // 5. Create refresh token
43 const refreshToken = crypto.randomUUID();
44 const refreshHash = await bcrypt.hash(refreshToken, 10);
45 await db.query(
46 `INSERT INTO refresh_tokens (user_id, session_id, token_hash, expires_at)
47 VALUES ($1, (SELECT id FROM sessions WHERE token_jti = $2),
48 $3, NOW() + INTERVAL '30 days')`,
49 [user.id, payload.jti, refreshHash]
50 );
51
52 return { token, refreshToken };
53}

6. Session Middleware

Intercept every API request, validate the QAuth token, and pass user info to route handlers.

Next.js Middleware

middleware.ts
1// middleware.ts (Next.js App Router)
2import { NextRequest, NextResponse } from 'next/server';
3import { QAuthValidator } from '@quantumshield/qauth';
4
5const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/signup', '/api/health'];
6
7// Pre-configure validator with your server's public keys
8const validator = new QAuthValidator(publicKeys, {
9 issuer: 'https://auth.yourapp.com',
10 audience: 'https://api.yourapp.com',
11});
12
13export function middleware(request: NextRequest) {
14 // Skip public paths
15 if (PUBLIC_PATHS.some(p => request.nextUrl.pathname.startsWith(p))) {
16 return NextResponse.next();
17 }
18
19 // Extract token
20 const authHeader = request.headers.get('authorization');
21 if (!authHeader?.startsWith('QAuth ')) {
22 return NextResponse.json({ error: 'Missing token' }, { status: 401 });
23 }
24 const token = authHeader.slice(6);
25
26 // Validate token
27 try {
28 const payload = validator.validate(token);
29
30 // Pass user info to route handlers via headers
31 const response = NextResponse.next();
32 response.headers.set('x-user-id', payload.sub);
33 response.headers.set('x-user-claims', JSON.stringify(payload.cst));
34 return response;
35 } catch (error) {
36 return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
37 }
38}
39
40export const config = {
41 matcher: '/api/:path*',
42};

Express.js Middleware

middleware.ts
1// middleware.ts (Express.js)
2import { Request, Response, NextFunction } from 'express';
3import { QAuthValidator, ProofValidator, TokenPayload } from '@quantumshield/qauth';
4
5const validator = new QAuthValidator(publicKeys, {
6 issuer: 'https://auth.yourapp.com',
7 audience: 'https://api.yourapp.com',
8});
9
10// Extend Express Request
11declare global {
12 namespace Express {
13 interface Request {
14 user?: TokenPayload;
15 }
16 }
17}
18
19export function qAuthMiddleware(req: Request, res: Response, next: NextFunction) {
20 const authHeader = req.headers.authorization;
21 if (!authHeader?.startsWith('QAuth ')) {
22 return res.status(401).json({ error: 'Missing token' });
23 }
24 const token = authHeader.slice(6);
25
26 try {
27 // Validate token
28 const payload = validator.validate(token);
29 req.user = payload;
30
31 // Optionally validate proof of possession
32 const proof = req.headers['x-qauth-proof'] as string;
33 if (proof) {
34 const clientKey = payload.cst.clientKey as string;
35 if (clientKey) {
36 const proofValidator = new ProofValidator(
37 new Uint8Array(Buffer.from(clientKey, 'hex'))
38 );
39 const isValid = proofValidator.validate(
40 proof, req.method, req.originalUrl, token, req.body
41 );
42 if (!isValid) {
43 return res.status(401).json({ error: 'Invalid proof' });
44 }
45 }
46 }
47
48 next();
49 } catch (error) {
50 return res.status(401).json({ error: 'Invalid token' });
51 }
52}

7. Protected API Routes

Validate the token (done by middleware), extract user info, then check policy authorization.

app/api/projects/route.ts
1// app/api/projects/route.ts (Next.js App Router)
2import { NextRequest, NextResponse } from 'next/server';
3import { policyEngine } from '@/lib/server';
4import { db } from '@/lib/database';
5
6export async function GET(request: NextRequest) {
7 // User info injected by middleware
8 const userId = request.headers.get('x-user-id');
9 const claims = JSON.parse(request.headers.get('x-user-claims') || '{}');
10
11 // Check policy
12 const authResult = policyEngine.evaluate('urn:qauth:policy:app', {
13 subject: {
14 id: userId!,
15 attributes: { role: claims.roles?.[0] },
16 },
17 resource: { path: 'api/projects' },
18 request: { action: 'list' },
19 });
20
21 if (authResult.effect !== 'allow') {
22 return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
23 }
24
25 // Fetch user's projects
26 const result = await db.query(
27 'SELECT * FROM projects WHERE owner_id = $1 ORDER BY created_at DESC',
28 [userId]
29 );
30
31 return NextResponse.json({ projects: result.rows });
32}

8. Token Refresh

Refresh token rotation: the old refresh token is revoked, and a new pair (access + refresh) is issued. This detects token theft — if a revoked token is reused, invalidate all sessions for that user.

lib/auth/refresh.ts
1import { qauth } from './server';
2import bcrypt from 'bcrypt';
3import { db } from './database';
4
5export async function refreshTokens(refreshToken: string) {
6 // 1. Find valid refresh token
7 const tokens = await db.query(
8 `SELECT rt.*, s.user_id
9 FROM refresh_tokens rt
10 JOIN sessions s ON rt.session_id = s.id
11 WHERE rt.revoked = false AND rt.expires_at > NOW()`
12 );
13
14 // 2. Find matching token (compare hashes)
15 let matchedToken = null;
16 for (const rt of tokens.rows) {
17 if (await bcrypt.compare(refreshToken, rt.token_hash)) {
18 matchedToken = rt;
19 break;
20 }
21 }
22 if (!matchedToken) {
23 throw new Error('Invalid refresh token');
24 }
25
26 // 3. Revoke old refresh token (rotation)
27 await db.query(
28 'UPDATE refresh_tokens SET revoked = true WHERE id = $1',
29 [matchedToken.id]
30 );
31
32 // 4. Get user
33 const userResult = await db.query(
34 'SELECT id, email, roles, display_name FROM users WHERE id = $1',
35 [matchedToken.user_id]
36 );
37 const user = userResult.rows[0];
38
39 // 5. Create new QAuth token
40 const newToken = qauth.createToken({
41 subject: user.id,
42 policyRef: 'urn:qauth:policy:app',
43 validitySeconds: 3600,
44 claims: { email: user.email, roles: user.roles },
45 });
46
47 // 6. Create new session
48 const payload = qauth.validateToken(newToken);
49 await db.query(
50 `INSERT INTO sessions (user_id, token_jti, expires_at)
51 VALUES ($1, $2, to_timestamp($3))`,
52 [user.id, payload.jti, payload.exp]
53 );
54
55 // 7. Create new refresh token
56 const newRefreshToken = crypto.randomUUID();
57 const refreshHash = await bcrypt.hash(newRefreshToken, 10);
58 await db.query(
59 `INSERT INTO refresh_tokens (user_id, session_id, token_hash, expires_at)
60 VALUES ($1, (SELECT id FROM sessions WHERE token_jti = $2),
61 $3, NOW() + INTERVAL '30 days')`,
62 [user.id, payload.jti, refreshHash]
63 );
64
65 return { token: newToken, refreshToken: newRefreshToken };
66}

9. Logout

Delete the session record (cascades to refresh tokens). The access token remains valid until expiry (max 1 hour), but the session cannot be refreshed.

lib/auth/logout.ts
1import { db } from './database';
2import { qauth } from './server';
3
4export async function logout(token: string) {
5 try {
6 // 1. Validate token to get JTI
7 const payload = qauth.validateToken(token);
8
9 // 2. Delete session (cascades to refresh tokens)
10 await db.query(
11 'DELETE FROM sessions WHERE token_jti = $1',
12 [payload.jti]
13 );
14
15 return { success: true };
16 } catch {
17 // Token might be expired, still try to clean up
18 return { success: true };
19 }
20}
21
22// Express route example
23app.post('/api/auth/logout', qAuthMiddleware, async (req, res) => {
24 const token = req.headers.authorization!.slice(6);
25 await logout(token);
26 res.json({ success: true });
27});

10. Complete Express.js Server

A minimal but complete Express.js server with QAuth. Copy-paste ready.

server.ts
1import express from 'express';
2import { QAuthServer, PolicyEngine } from '@quantumshield/qauth';
3import bcrypt from 'bcrypt';
4import { Pool } from 'pg';
5
6const app = express();
7app.use(express.json());
8
9// Database
10const db = new Pool({ connectionString: process.env.DATABASE_URL });
11
12// QAuth
13const qauth = new QAuthServer({
14 issuer: 'https://auth.yourapp.com',
15 audience: 'https://api.yourapp.com',
16});
17
18// Auth middleware
19function requireAuth(req, res, next) {
20 const auth = req.headers.authorization;
21 if (!auth?.startsWith('QAuth ')) {
22 return res.status(401).json({ error: 'Missing token' });
23 }
24 try {
25 req.user = qauth.validateToken(auth.slice(6));
26 next();
27 } catch {
28 res.status(401).json({ error: 'Invalid token' });
29 }
30}
31
32// Signup
33app.post('/auth/signup', async (req, res) => {
34 const { email, password, name } = req.body;
35 const hash = await bcrypt.hash(password, 12);
36 const result = await db.query(
37 'INSERT INTO users (email, password_hash, display_name) VALUES ($1,$2,$3) RETURNING *',
38 [email, hash, name]
39 );
40 const user = result.rows[0];
41 const token = qauth.createToken({
42 subject: user.id,
43 policyRef: 'urn:qauth:policy:app',
44 claims: { email, roles: user.roles },
45 });
46 res.json({ token, user: { id: user.id, email, name } });
47});
48
49// Login
50app.post('/auth/login', async (req, res) => {
51 const { email, password } = req.body;
52 const result = await db.query('SELECT * FROM users WHERE email=$1', [email]);
53 const user = result.rows[0];
54 if (!user || !await bcrypt.compare(password, user.password_hash)) {
55 return res.status(401).json({ error: 'Invalid credentials' });
56 }
57 const token = qauth.createToken({
58 subject: user.id,
59 policyRef: 'urn:qauth:policy:app',
60 claims: { email, roles: user.roles },
61 });
62 res.json({ token });
63});
64
65// Protected route
66app.get('/api/me', requireAuth, async (req, res) => {
67 const result = await db.query('SELECT * FROM users WHERE id=$1', [req.user.sub]);
68 res.json({ user: result.rows[0] });
69});
70
71app.listen(3000, () => console.log('Server running on :3000'));

Next Steps