OAuth Authentication Setup Guide#
This project uses the atcute library for AT Protocol OAuth authentication, providing a lightweight and modern approach to user authentication with Bluesky and other AT Protocol services.
Overview#
The authentication system is built with:
- @atcute/oauth-node-client - OAuth client for Node.js environments
- @atcute/identity-resolver - Handle and DID resolution
- @atcute/client - AT Protocol API client
- JWT-based session management - Secure token signing with ES256
Setup Instructions#
1. Generate OAuth Private Key#
The OAuth system requires a private key for JWT signing. Generate one by running:
npm run setup:key
This will:
- Generate an ES256 private key
- Save it to
.envasPRIVATE_KEY_JWK - Update
.env.examplewith a placeholder
IMPORTANT: Keep this key secure and never commit it to version control!
2. Environment Configuration#
Ensure your .env file contains:
# Database
DATABASE_PATH=./data/app.db
# OAuth Configuration
PUBLIC_BASE_URL=http://localhost:5173
PRIVATE_KEY_JWK={"kty":"EC","x":"...","y":"...","crv":"P-256","d":"...","alg":"ES256"}
# Session (not currently used but reserved for future encryption)
SESSION_SECRET=change-this-to-a-random-secret-in-production
For production:
- Use HTTPS and update
PUBLIC_BASE_URLaccordingly - Generate a strong
SESSION_SECRETfor additional security
3. Start the Development Server#
npm run dev
The server will start on http://localhost:5173
Authentication Flow#
1. Login Endpoint (POST /auth/login)#
Users can log in with their Bluesky handle, DID, or PDS service URL:
// Example request
POST /auth/login
Content-Type: application/json
{
"handle": "alice.bsky.social" // or "did:plc:..." or "https://bsky.social"
}
Process:
- Validates the identifier format
- Determines the target type (account, DID, or PDS)
- Initiates OAuth authorization with the AT Protocol service
- Redirects user to the authorization page
2. OAuth Callback (GET /auth/callback)#
After user authorizes on their AT Protocol service:
- Receives authorization code via query parameters
- Exchanges code for access tokens
- Stores the user's DID in a secure HTTP-only cookie
- Redirects to home page
3. Session Management#
Sessions are managed through:
- Cookie Storage: User's DID stored in
go_sessioncookie - OAuth Session Store: In-memory store (MemoryStore) for session data
- Supports up to 10,000 sessions
- Includes automatic token refresh
- State Store: Temporary storage for OAuth state (10-minute TTL)
Session Restoration:
// The getSession() function automatically restores and refreshes OAuth sessions
const session = await getSession(event);
if (session) {
// User is authenticated
console.log('User DID:', session.did);
}
4. Logout Endpoint (POST /auth/logout)#
Properly signs out the user:
- Revokes OAuth tokens at the authorization server
- Clears local session cookie
- Redirects to home page
OAuth Discovery Endpoints#
The following endpoints are required for OAuth discovery:
/oauth-client-metadata.json#
Serves the OAuth client metadata including:
client_id- Identifies this applicationredirect_uris- Where users return after authorizationscope- Requested permissions (atproto)jwks_uri- Public key endpoint
/jwks.json#
Serves the JSON Web Key Set (JWKS) containing the public keys for token verification.
Identity Resolution#
The system uses comprehensive identity resolution:
Handle Resolution#
- DNS Resolution: Resolves handles via DNS TXT records
- HTTP Resolution: Falls back to
.well-knownendpoint
DID Document Resolution#
- PLC: Resolves
did:plc:*via PLC directory - Web: Resolves
did:web:*via HTTPS
Security Considerations#
Current Implementation#
- ✅ HTTP-only cookies prevent XSS attacks
- ✅ SameSite=Lax prevents CSRF attacks
- ✅ Automatic token refresh
- ✅ Proper token revocation on logout
- ✅ JWT signing with ES256
Production Recommendations#
- Use HTTPS: Enable
secure: truefor cookies - Persistent Storage: Replace
MemoryStorewith Redis or database - Session Encryption: Implement additional cookie encryption with
SESSION_SECRET - Rate Limiting: Add rate limits to auth endpoints
- Monitoring: Log authentication events for security auditing
Adapting for Production#
Replace In-Memory Stores#
For production deployments, replace the in-memory stores with persistent storage:
// Example with Redis (pseudo-code)
import { RedisStore } from './redis-store'; // implement this
const oauth = new OAuthClient({
// ...other config
stores: {
sessions: new RedisStore({
client: redisClient,
prefix: 'oauth:session:',
}),
states: new RedisStore({
client: redisClient,
prefix: 'oauth:state:',
ttl: TEN_MINUTES_MS,
}),
},
});
Cookie Encryption#
Update setSession() in src/lib/server/auth.ts to encrypt cookies:
import { createCipheriv, createDecipheriv } from 'crypto';
// Implement encryption/decryption using SESSION_SECRET
API Reference#
Auth Functions (src/lib/server/auth.ts)#
getOAuthClient(): Promise<OAuthClient>#
Returns the configured OAuth client instance (singleton).
getSession(event: RequestEvent): Promise<Session | null>#
Retrieves and validates the current user session.
- Automatically refreshes expired tokens
- Returns
nullif session is invalid
setSession(event: RequestEvent, session: Session): Promise<void>#
Stores a new session in a secure cookie.
clearSession(event: RequestEvent): Promise<void>#
Removes the session cookie.
Session Interface#
interface Session {
did: string; // Decentralized Identifier (e.g., "did:plc:...")
handle?: string; // Optional AT Protocol handle
}
Troubleshooting#
"PRIVATE_KEY_JWK environment variable is required"#
Run npm run setup:key to generate the private key.
"OAuth callback error"#
Check that:
PUBLIC_BASE_URLis correctly set in.env- The URL is accessible from the browser
- For local development, use
http://localhost:5173(not127.0.0.1)
Session not persisting#
- Check browser console for cookie errors
- Ensure cookies are enabled
- Verify
PUBLIC_BASE_URLmatches your actual URL
Token refresh failures#
- The MemoryStore loses data on server restart (expected in development)
- For production, implement persistent storage
Testing the Implementation#
Manual Testing#
- Navigate to
http://localhost:5173 - Click "Login" and enter your Bluesky handle (e.g.,
alice.bsky.social) - Authorize the application on Bluesky
- You should be redirected back and see your session
Test with curl#
# Login (get redirect URL)
curl -X POST http://localhost:5173/auth/login \
-H "Content-Type: application/json" \
-d '{"handle":"alice.bsky.social"}' \
-i
# Check metadata
curl http://localhost:5173/oauth-client-metadata.json | jq
# Check JWKS
curl http://localhost:5173/jwks.json | jq
Resources#
Next Steps#
- Implement persistent session storage (Redis/PostgreSQL)
- Add session encryption with SESSION_SECRET
- Implement rate limiting
- Add authentication middleware for protected routes
- Fetch and display user handle/profile
- Add refresh token rotation
- Implement proper error pages
- Add security headers (HSTS, CSP, etc.)