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:
84
lib/auth/oauth-session-store.ts
Normal file
84
lib/auth/oauth-session-store.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user