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