extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.

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 .env as PRIVATE_KEY_JWK
  • Update .env.example with 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_URL accordingly
  • Generate a strong SESSION_SECRET for 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:

  1. Validates the identifier format
  2. Determines the target type (account, DID, or PDS)
  3. Initiates OAuth authorization with the AT Protocol service
  4. Redirects user to the authorization page

2. OAuth Callback (GET /auth/callback)#

After user authorizes on their AT Protocol service:

  1. Receives authorization code via query parameters
  2. Exchanges code for access tokens
  3. Stores the user's DID in a secure HTTP-only cookie
  4. Redirects to home page

3. Session Management#

Sessions are managed through:

  • Cookie Storage: User's DID stored in go_session cookie
  • 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:

  1. Revokes OAuth tokens at the authorization server
  2. Clears local session cookie
  3. 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 application
  • redirect_uris - Where users return after authorization
  • scope - 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-known endpoint

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#

  1. Use HTTPS: Enable secure: true for cookies
  2. Persistent Storage: Replace MemoryStore with Redis or database
  3. Session Encryption: Implement additional cookie encryption with SESSION_SECRET
  4. Rate Limiting: Add rate limits to auth endpoints
  5. 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,
    }),
  },
});

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 null if 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:

  1. PUBLIC_BASE_URL is correctly set in .env
  2. The URL is accessible from the browser
  3. For local development, use http://localhost:5173 (not 127.0.0.1)

Session not persisting#

  1. Check browser console for cookie errors
  2. Ensure cookies are enabled
  3. Verify PUBLIC_BASE_URL matches your actual URL

Token refresh failures#

  1. The MemoryStore loses data on server restart (expected in development)
  2. For production, implement persistent storage

Testing the Implementation#

Manual Testing#

  1. Navigate to http://localhost:5173
  2. Click "Login" and enter your Bluesky handle (e.g., alice.bsky.social)
  3. Authorize the application on Bluesky
  4. 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.)