Files
app/lib/auth/session.ts
Albert d7f3bcd338 feat: Implement OAuth with DPoP using @atproto/oauth-client-node
Replace manual OAuth implementation with official @atproto/oauth-client-node library to properly support DPoP (Demonstrating Proof of Possession) authentication.

Changes:
- Added @atproto/oauth-client-node dependency
- Created OAuth state store (SurrealDB-backed) for CSRF protection
- Created OAuth session store (SurrealDB-backed) for token persistence
- Created OAuth client singleton with localhost exception for development
- Rewrote /api/auth/login to use client.authorize()
- Rewrote /api/auth/callback to use client.callback() with DPoP
- Updated lib/auth/session.ts with getAuthenticatedAgent() for ATproto API calls
- Updated db/schema.surql with oauth_state and oauth_session tables
- Added scripts/apply-schema.js for database schema management
- Created plans/oauth-dpop-implementation.md with detailed implementation plan
- Removed legacy lib/auth/atproto.ts and lib/auth/oauth-state.ts
- Updated .env to use localhost exception (removed BLUESKY_CLIENT_ID)

The OAuth client now handles:
- PKCE code generation and verification
- DPoP proof generation and signing
- Automatic token refresh
- Session persistence across server restarts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 01:40:04 +00:00

124 lines
3.4 KiB
TypeScript

import { cookies } from 'next/headers';
import { verifySurrealJwt, type UserSession } from './jwt';
import { getOAuthClient } from './oauth-client';
import { Agent } from '@atproto/api';
import jwt from 'jsonwebtoken';
/**
* Gets the current authenticated user from the session cookie.
* This function should be called from Server Components or API routes.
*
* @returns The user session if authenticated, null otherwise
*/
export async function getCurrentUser(): Promise<UserSession | null> {
const cookieStore = await cookies();
const authCookie = cookieStore.get('ponderants-auth');
if (!authCookie?.value) {
return null;
}
return verifySurrealJwt(authCookie.value);
}
/**
* Get the authenticated ATproto agent for the current user.
*
* Automatically refreshes tokens if needed. The OAuth client
* handles token refresh transparently and updates the sessionStore.
*
* @returns Agent instance for making authenticated ATproto API calls,
* or null if user is not authenticated
*/
export async function getAuthenticatedAgent(): Promise<Agent | null> {
try {
// Get user DID from our SurrealDB JWT cookie
const cookieStore = await cookies();
const authCookie = cookieStore.get('ponderants-auth');
if (!authCookie) {
return null;
}
// Decode JWT to get DID (we don't verify here since SurrealDB will verify)
const payload = jwt.decode(authCookie.value) as { sub: string } | null;
if (!payload?.sub) {
return null;
}
const did = payload.sub;
console.log('[Session] Restoring session for DID:', did);
// Restore session from OAuth client
// This will automatically refresh tokens if they're expired
const client = await getOAuthClient();
const session = await client.restore(did);
console.log('[Session] ✓ Session restored');
// Create agent with session
return new Agent(session);
} catch (error) {
console.error('[Session] Failed to restore session:', error);
return null;
}
}
/**
* Sign out the current user.
*
* Revokes tokens with the PDS and clears the session from storage.
* Also clears our app's JWT cookie.
*/
export async function signOut(): Promise<void> {
try {
const agent = await getAuthenticatedAgent();
if (agent) {
console.log('[Session] Signing out user:', agent.did);
// Revoke session (calls PDS to revoke tokens)
const session = (agent as any).session;
if (session?.signOut) {
await session.signOut();
console.log('[Session] ✓ Session revoked with PDS');
}
}
} catch (error) {
console.error('[Session] Sign out error:', error);
// Continue to clear cookie even if revocation fails
}
// Clear our app's cookie
const cookieStore = await cookies();
cookieStore.delete('ponderants-auth');
console.log('[Session] ✓ Local session cleared');
}
/**
* Get the current user's DID from the session cookie.
*
* This is a lightweight check that doesn't require restoring
* the full OAuth session. Useful for quick authorization checks.
*
* @returns User's DID or null if not authenticated
*/
export async function getCurrentUserDid(): Promise<string | null> {
try {
const cookieStore = await cookies();
const authCookie = cookieStore.get('ponderants-auth');
if (!authCookie) {
return null;
}
const payload = jwt.decode(authCookie.value) as { sub: string } | null;
return payload?.sub || null;
} catch {
return null;
}
}