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>
85 lines
2.3 KiB
TypeScript
85 lines
2.3 KiB
TypeScript
/**
|
|
* OAuth Session Store for @atproto/oauth-client-node
|
|
*
|
|
* Stores persistent OAuth sessions (access/refresh tokens, DPoP keys).
|
|
* Sessions are keyed by the user's DID.
|
|
*/
|
|
|
|
import Surreal from 'surrealdb';
|
|
import type { NodeSavedSessionStore, NodeSavedSession } from '@atproto/oauth-client-node';
|
|
|
|
/**
|
|
* Get a SurrealDB connection with root credentials.
|
|
* Used for OAuth session 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 session store backed by SurrealDB.
|
|
*
|
|
* The session store persists authenticated user sessions across
|
|
* server restarts. The @atproto/oauth-client-node library manages
|
|
* token refresh automatically, updating the store when tokens change.
|
|
*
|
|
* Sessions are indexed by DID (decentralized identifier).
|
|
*/
|
|
export function createSessionStore(): NodeSavedSessionStore {
|
|
return {
|
|
async set(did: string, sessionData: NodeSavedSession): Promise<void> {
|
|
const db = await getDB();
|
|
|
|
try {
|
|
// Upsert: create if doesn't exist, update if it does
|
|
await db.query(
|
|
`INSERT INTO oauth_session (did, session_data)
|
|
VALUES ($did, $session_data)
|
|
ON DUPLICATE KEY UPDATE session_data = $session_data, updated_at = time::now()`,
|
|
{ did, session_data: sessionData }
|
|
);
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
},
|
|
|
|
async get(did: string): Promise<NodeSavedSession | undefined> {
|
|
const db = await getDB();
|
|
|
|
try {
|
|
const [result] = await db.query<[{ session_data: NodeSavedSession }[]]>(
|
|
'SELECT session_data FROM oauth_session WHERE did = $did',
|
|
{ did }
|
|
);
|
|
|
|
return result?.[0]?.session_data;
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
},
|
|
|
|
async del(did: string): Promise<void> {
|
|
const db = await getDB();
|
|
|
|
try {
|
|
await db.query(
|
|
'DELETE oauth_session WHERE did = $did',
|
|
{ did }
|
|
);
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
},
|
|
};
|
|
}
|