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>
This commit is contained in:
75
lib/auth/oauth-client.ts
Normal file
75
lib/auth/oauth-client.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* OAuth Client Singleton for ATproto
|
||||
*
|
||||
* This module provides a singleton instance of NodeOAuthClient
|
||||
* that manages OAuth flows, DPoP proofs, and session persistence.
|
||||
*/
|
||||
|
||||
import { NodeOAuthClient } from '@atproto/oauth-client-node';
|
||||
import { createStateStore } from './oauth-state-store';
|
||||
import { createSessionStore } from './oauth-session-store';
|
||||
|
||||
let clientInstance: NodeOAuthClient | null = null;
|
||||
|
||||
/**
|
||||
* Get or create the singleton OAuth client instance.
|
||||
*
|
||||
* In development, uses the localhost client exception (no keys needed).
|
||||
* In production, uses backend service with private keys (TODO).
|
||||
*
|
||||
* The client handles:
|
||||
* - OAuth authorization flow with PKCE
|
||||
* - DPoP (Demonstrating Proof of Possession) for token requests
|
||||
* - Automatic token refresh
|
||||
* - Session persistence in SurrealDB
|
||||
*/
|
||||
export async function getOAuthClient(): Promise<NodeOAuthClient> {
|
||||
if (clientInstance) {
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const callbackUrl = process.env.BLUESKY_REDIRECT_URI;
|
||||
|
||||
if (!callbackUrl) {
|
||||
throw new Error('BLUESKY_REDIRECT_URI environment variable is required');
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
// Development: Use localhost exception
|
||||
// Per ATproto spec, client_id must be exactly "http://localhost"
|
||||
// (no port number) with metadata in query parameters
|
||||
const clientId = `http://localhost?${new URLSearchParams({
|
||||
redirect_uri: callbackUrl,
|
||||
scope: 'atproto',
|
||||
})}`;
|
||||
|
||||
console.log('[OAuth] Initializing development client with localhost exception');
|
||||
console.log('[OAuth] client_id:', clientId);
|
||||
|
||||
clientInstance = await NodeOAuthClient.fromClientId({
|
||||
clientId,
|
||||
stateStore: createStateStore(),
|
||||
sessionStore: createSessionStore(),
|
||||
});
|
||||
|
||||
console.log('[OAuth] ✓ Development client initialized');
|
||||
} else {
|
||||
// Production: Backend service with keys
|
||||
// TODO: Implement when deploying to production
|
||||
// See plans/oauth-dpop-implementation.md for details
|
||||
throw new Error(
|
||||
'Production OAuth client not yet implemented. ' +
|
||||
'See plans/oauth-dpop-implementation.md for production setup instructions.'
|
||||
);
|
||||
}
|
||||
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the singleton instance (mainly for testing).
|
||||
*/
|
||||
export function clearOAuthClient(): void {
|
||||
clientInstance = null;
|
||||
}
|
||||
Reference in New Issue
Block a user