Pocket ID Implementation: Complete Handoff Document
A comprehensive guide for implementing Pocket ID with oauth2-proxy to protect a Hugo static site. Includes architecture, failure modes, validation steps, and handoff-ready instructions.
Context
You have a Hugo static site. You want a private section that only you and your friends can access using passkeys (WebAuthn). This document provides everything needed to implement that using Pocket ID as the authentication provider.
The Problem
Hugo generates static HTML files. WebAuthn requires:
- Server-side challenge generation
- Session state management
- Token verification
Solution: Put a stateful authentication layer in front of your static files. The static files remain static; the auth happens at the edge.
Architecture Overview
┌─────────────┐ ┌──────────────────────────────────────────────┐
│ Browser │────▶│ Nginx │
└─────────────┘ │ ┌─────────────────────────────────────────┐ │
│ │ /private/* → auth_request → oauth2-proxy│ │
│ │ /* → static files │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
│
▼ auth check
┌───────────────────────┐
│ oauth2-proxy │
│ (OIDC client) │
└───────────────────────┘
│
▼ token validation
┌───────────────────────┐
│ Pocket ID │
│ (OIDC Provider) │
│ WebAuthn backend │
└───────────────────────┘
Why Pocket ID
| Factor | Pocket ID | Authentik | Keycloak |
|---|---|---|---|
| Container count | 1 | 2+ | 2+ |
| Database | SQLite (default) | PostgreSQL | PostgreSQL |
| Passkey-first | ✅ Yes | ✅ Yes | ⚠️ Requires config |
| Setup complexity | Low | Medium | High |
| Resource usage | ~50MB RAM | ~200MB+ | ~500MB+ |
| Good for 2-5 users | ✅ Ideal | ✅ Good | ❌ Overkill |
Recommendation: Pocket ID for 2-3 users. Single container, SQLite, passkey-native.
Prerequisites
Before starting, you need:
- Domain with DNS control - e.g.,
id.ch3ngl0rd.comfor Pocket ID - TLS certificates - WebAuthn requires HTTPS
- Docker and Docker Compose - Container runtime
- Nginx - Already serving your Hugo site
- A private path - e.g.,
/private/on your site
Implementation Steps
Step 1: Create Docker Network
docker network create auth-network
Step 2: Create Pocket ID Configuration
Create ~/pocket-id/docker-compose.yml:
version: "3.8"
services:
pocket-id:
image: ghcr.io/pocket-id/pocket-id:latest
container_name: pocket-id
restart: unless-stopped
environment:
# Public URL - MUST match your domain exactly
- PUBLIC_APP_URL=https://id.ch3ngl0rd.com
# Trust reverse proxy headers
- TRUST_PROXY=1
# SQLite (default) - good for small deployments
- DB_PROVIDER=sqlite
# Session settings
- SESSION_DURATION=720h # 30 days
# Optional: GeoIP for login insights
# - MAXMIND_LICENSE_KEY=your-key
volumes:
- ./data:/app/data
networks:
- auth-network
ports:
- "127.0.0.1:1411:1411" # Only expose to localhost
networks:
auth-network:
external: true
Critical configuration notes:
PUBLIC_APP_URLmust be the exact HTTPS URL users accessTRUST_PROXY=1is required when behind Nginx- Port only bound to localhost - Nginx handles external TLS
Step 3: Create oauth2-proxy Configuration
Create ~/oauth2-proxy/docker-compose.yml:
version: "3.8"
services:
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
container_name: oauth2-proxy
restart: unless-stopped
command:
- --provider=oidc
- --client-id=pocket-id-client
- --client-secret=YOUR_CLIENT_SECRET
- --oidc-issuer-url=https://id.ch3ngl0rd.com
- --redirect-url=https://agents.ch3ngl0rd.com/oauth2/callback
- --cookie-secret=YOUR_COOKIE_SECRET
- --cookie-secure=true
- --cookie-name=_oauth2_proxy
- --email-domain=*
- --upstream=static://200
- --skip-provider-button=true
- --pass-authorization-header=true
- --set-authorization-header=true
- --pass-access-token=true
# Increase cookie buffer for large tokens
- --cookie-csrf-per-request=true
- --cookie-csrf-expire=5m
networks:
- auth-network
ports:
- "127.0.0.1:4180:4180"
networks:
auth-network:
external: true
Generate secrets:
# Cookie secret (32 bytes, base64)
openssl rand -base64 32
# Client secret (will be set in Pocket ID admin UI)
Step 4: Configure Nginx
Add to your existing Nginx config:
# Upstream for oauth2-proxy
upstream oauth2-proxy {
server 127.0.0.1:4180;
}
# Pocket ID (for auth provider)
server {
listen 443 ssl http2;
server_name id.ch3ngl0rd.com;
ssl_certificate /etc/letsencrypt/live/ch3ngl0rd.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ch3ngl0rd.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:1411;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Protected paths on main site
server {
listen 443 ssl http2;
server_name agents.ch3ngl0rd.com;
# ... existing SSL config ...
# OAuth callback - must be publicly accessible
location /oauth2/ {
proxy_pass http://oauth2-proxy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Important for large cookies
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# Protected section
location /private/ {
auth_request /oauth2/auth;
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
# Pass user info to backend if needed
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
# Serve static files
alias /var/www/agents/private/;
try_files $uri $uri/ =404;
}
# Public content - no auth
location / {
root /var/www/agents;
try_files $uri $uri/ =404;
}
}
Step 5: Start Services
# Start Pocket ID first
cd ~/pocket-id && docker compose up -d
# Wait for it to be ready
sleep 5
# Verify it's running
curl -k https://localhost:1411/health || curl http://localhost:1411/
# Start oauth2-proxy
cd ~/oauth2-proxy && docker compose up -d
Step 6: Create OIDC Client in Pocket ID
- Navigate to
https://id.ch3ngl0rd.com - Create your admin account (first user becomes admin)
- Register your passkey
- Go to Clients → Create Client
- Configure:
- Name:
agents-ch3ngl0rd - Client ID:
pocket-id-client(or auto-generated) - Redirect URIs:
https://agents.ch3ngl0rd.com/oauth2/callback - Scopes:
openid,profile,email
- Name:
- Copy the Client Secret and update oauth2-proxy config
Step 7: Add Your Friend as User
- In Pocket ID admin, go to Users
- Click Add User
- Enter their email
- They will receive an email to register their passkey
- Alternatively, share an invite link
Common Failure Modes
1. RP ID / Origin Mismatch
Symptoms:
- WebAuthn registration fails with “NotAllowedError”
- Authentication silently fails
- Browser console shows origin mismatch
Root cause: The PUBLIC_APP_URL in Pocket ID doesn’t match the actual browser URL.
Fix:
# Verify PUBLIC_APP_URL matches exactly
docker exec pocket-id env | grep PUBLIC_APP_URL
# Must be: https://id.ch3ngl0rd.com (no trailing slash)
# NOT: http://id.ch3ngl0rd.com
# NOT: https://id.ch3ngl0rd.com/
Validation:
# Check browser URL matches RP ID
# Open browser DevTools → Console during passkey registration
# Look for: "RP ID: id.ch3ngl0rd.com"
2. Cookie Size Issues
Symptoms:
- 400 Bad Request from oauth2-proxy
- Upstream sent too big header
- Intermittent auth failures
Root cause: OAuth tokens can be large. Nginx default buffer is too small.
Fix:
# In Nginx server block
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# In oauth2-proxy
--cookie-csrf-per-request=true
--cookie-csrf-expire=5m
3. HTTPS Required
Symptoms:
- WebAuthn API not available
- navigator.credentials is undefined
- “NotSupportedError” during registration
Root cause: WebAuthn only works on secure contexts (HTTPS or localhost).
Fix:
- Use HTTPS everywhere in production
- For local testing,
localhostis treated as secure
Validation:
// In browser console
console.log(window.isSecureContext); // Must be true
console.log(window.PublicKeyCredential); // Must be defined
4. Clock Skew
Symptoms:
- Token validation fails
- “Token expired” immediately after login
- Intermittent auth failures across services
Root cause: Server clocks not synchronized.
Fix:
# Enable NTP sync
sudo timedatectl set-ntp true
# Verify
timedatectl status
5. Proxy Headers Not Trusted
Symptoms:
- Redirect loops
- Wrong scheme in redirect URLs
- Pocket ID shows http:// instead of https://
Root cause: Missing TRUST_PROXY=1 or Nginx not sending proxy headers.
Fix:
# In Pocket ID docker-compose
- TRUST_PROXY=1
# In Nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
6. Lost Passkey Recovery
Symptoms:
- User cannot authenticate
- No backup authentication method
Root cause: No recovery mechanism configured.
Fix:
- Prevent: Register multiple passkeys per user (different devices)
- Recover: Admin can reset user’s passkeys in Pocket ID UI
- Backup: Consider adding email-based backup codes
Validation Checklist
Pocket ID Health
# 1. Container running
docker ps | grep pocket-id
# 2. Health check
curl -f https://id.ch3ngl0rd.com/health && echo "OK"
# 3. OIDC discovery
curl https://id.ch3ngl0rd.com/.well-known/openid-configuration | jq .
# 4. Database accessible
docker exec pocket-id ls -la /app/data/
oauth2-proxy Health
# 1. Container running
docker ps | grep oauth2-proxy
# 2. Auth endpoint responds
curl -I http://localhost:4180/oauth2/auth
# Should return 401 (not authenticated) or 202 (authenticated)
# 3. Check logs
docker logs oauth2-proxy --tail 50
Nginx Integration
# 1. Config valid
sudo nginx -t
# 2. Protected path returns 401 without auth
curl -I https://agents.ch3ngl0rd.com/private/
# Should return 401 or redirect to auth
# 3. OAuth callback accessible
curl -I https://agents.ch3ngl0rd.com/oauth2/sign_in
# Should return redirect to Pocket ID
End-to-End Flow
- Navigate to protected path:
https://agents.ch3ngl0rd.com/private/ - Should redirect to:
https://id.ch3ngl0rd.com/login - Complete passkey authentication
- Should redirect back to: Original protected path
- Verify cookie set: Check browser DevTools → Application → Cookies
Rollback Procedure
If something goes wrong:
# 1. Stop auth services
docker stop oauth2-proxy pocket-id
# 2. Remove Nginx auth config (comment out auth_request lines)
sudo nano /etc/nginx/sites-available/agents
# 3. Reload Nginx
sudo nginx -t && sudo systemctl reload nginx
# 4. Site now fully public again
Monitoring and Maintenance
Logs to Watch
# Pocket ID
docker logs -f pocket-id
# oauth2-proxy
docker logs -f oauth2-proxy
# Nginx auth requests
sudo tail -f /var/log/nginx/access.log | grep -E "(oauth2|private)"
Backup Strategy
# Pocket ID data (SQLite + keys)
tar -czvf pocket-id-backup-$(date +%Y%m%d).tar.gz ~/pocket-id/data/
# Schedule weekly backups
echo "0 3 * * 0 tar -czvf /backup/pocket-id-\$(date +\%Y\%m\%d).tar.gz ~/pocket-id/data/" | crontab -
Updates
# Pull latest images
docker pull ghcr.io/pocket-id/pocket-id:latest
docker pull quay.io/oauth2-proxy/oauth2-proxy:latest
# Recreate containers
cd ~/pocket-id && docker compose up -d
cd ~/oauth2-proxy && docker compose up -d
Summary
| Component | Purpose | Port |
|---|---|---|
| Pocket ID | OIDC provider, WebAuthn backend | 1411 (localhost) |
| oauth2-proxy | OIDC client, cookie management | 4180 (localhost) |
| Nginx | TLS termination, auth_request routing | 443 (public) |
Key success factors:
- Exact match between
PUBLIC_APP_URLand browser URL TRUST_PROXY=1when behind Nginx- Large enough Nginx buffers for OAuth cookies
- HTTPS everywhere
- Multiple passkeys per user for recovery