Files
app/lib/auth/oauth-state-store.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

82 lines
2.0 KiB
TypeScript

/**
* OAuth State Store for @atproto/oauth-client-node
*
* Stores temporary OAuth state during the authorization flow.
* Used for CSRF protection and PKCE verification.
*/
import Surreal from 'surrealdb';
import type { NodeSavedStateStore, NodeSavedState } from '@atproto/oauth-client-node';
/**
* Get a SurrealDB connection with root credentials.
* Used for OAuth state management.
*/
async function getDB(): Promise<Surreal> {
const db = new Surreal();
await db.connect(process.env.SURREALDB_URL!);
await db.signin({
username: process.env.SURREALDB_USER!,
password: process.env.SURREALDB_PASS!,
});
await db.use({
namespace: process.env.SURREALDB_NS!,
database: process.env.SURREALDB_DB!,
});
return db;
}
/**
* Create an OAuth state store backed by SurrealDB.
*
* The state store is used during the OAuth flow to store
* temporary data (PKCE verifier, DPoP key, etc.) that is
* retrieved when the user returns from the authorization server.
*
* States expire after 1 hour (enforced by database event).
*/
export function createStateStore(): NodeSavedStateStore {
return {
async set(key: string, value: NodeSavedState): Promise<void> {
const db = await getDB();
try {
await db.query(
'CREATE oauth_state SET key = $key, value = $value',
{ key, value }
);
} finally {
await db.close();
}
},
async get(key: string): Promise<NodeSavedState | undefined> {
const db = await getDB();
try {
const [result] = await db.query<[{ value: NodeSavedState }[]]>(
'SELECT value FROM oauth_state WHERE key = $key',
{ key }
);
return result?.[0]?.value;
} finally {
await db.close();
}
},
async del(key: string): Promise<void> {
const db = await getDB();
try {
await db.query(
'DELETE oauth_state WHERE key = $key',
{ key }
);
} finally {
await db.close();
}
},
};
}