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>
82 lines
2.0 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
};
|
|
}
|