@spfn/auth 0.2.0-beta.6 → 0.2.0-beta.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +831 -198
- package/dist/{dto-Bb2qFUO6.d.ts → authenticate-B_HkYBzq.d.ts} +449 -199
- package/dist/config.d.ts +176 -44
- package/dist/config.js +99 -35
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +289 -113
- package/dist/index.js +59 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +547 -47
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.d.ts +28 -0
- package/dist/nextjs/client.js +80 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/server.d.ts +92 -3
- package/dist/nextjs/server.js +282 -22
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +860 -468
- package/dist/server.js +1454 -607
- package/dist/server.js.map +1 -1
- package/dist/session-Dbvz9Sdp.d.ts +53 -0
- package/dist/types-B1CzVZkU.d.ts +45 -0
- package/migrations/0001_smooth_the_fury.sql +3 -0
- package/migrations/0002_deep_iceman.sql +11 -0
- package/migrations/0003_perfect_deathbird.sql +3 -0
- package/migrations/0004_concerned_rawhide_kid.sql +5 -0
- package/migrations/meta/0001_snapshot.json +1660 -0
- package/migrations/meta/0002_snapshot.json +1660 -0
- package/migrations/meta/0003_snapshot.json +1689 -0
- package/migrations/meta/0004_snapshot.json +1721 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +15 -11
package/dist/nextjs/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/nextjs/api.ts","../../src/nextjs/interceptors/login-register.ts","../../src/nextjs/interceptors/general-auth.ts","../../src/nextjs/interceptors/key-rotation.ts","../../src/nextjs/interceptors/index.ts"],"sourcesContent":["/**\n * @spfn/auth/adapters/nextjs/api\n *\n * Next.js Adapter for SPFN Auth\n *\n * Provides automatic interceptor registration for seamless auth flow:\n * - Session management (HttpOnly cookies)\n * - JWT generation and signing\n * - Public key encryption\n *\n * @requires next >= 13.0.0\n *\n * @example\n * ```typescript\n * // Just import to auto-register interceptors\n * import '@spfn/auth/nextjs/api';\n * ```\n */\n\n// Re-export interceptors for advanced usage\nimport { registerInterceptors } from \"@spfn/core/nextjs/server\";\nimport { authInterceptors } from './interceptors';\n\n// Auto-register interceptors on import\nregisterInterceptors('auth', authInterceptors);","/**\n * Login/Register Interceptor\n *\n * Automatically handles key generation and session management\n * for login and register endpoints\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs';\nimport { generateKeyPair, sealSession, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\nimport { env } from '@spfn/core/config';\n\n/**\n * Login and Register Interceptor\n *\n * Request: Generates key pair and adds publicKey to request body\n * Response: Saves privateKey to HttpOnly cookie\n */\nexport const loginRegisterInterceptor: InterceptorRule =\n {\n pathPattern: /^\\/_auth\\/(login|register)$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n // Get old session if exists (for key rotation on login)\n const oldKeyId = ctx.cookies.get(COOKIE_NAMES.SESSION_KEY_ID);\n\n // Extract remember option from request body (if provided)\n const remember = ctx.body?.remember;\n\n // Generate new key pair\n const keyPair = generateKeyPair('ES256');\n\n // Add publicKey data to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = keyPair.publicKey;\n ctx.body.keyId = keyPair.keyId;\n ctx.body.fingerprint = keyPair.fingerprint;\n ctx.body.algorithm = keyPair.algorithm;\n ctx.body.keySize = Buffer.from(keyPair.publicKey, 'base64').length;\n\n // Add oldKeyId for login (key rotation)\n if (ctx.path === '/_auth/login' && oldKeyId)\n {\n ctx.body.oldKeyId = oldKeyId;\n }\n\n // Remove remember from body (not part of contract)\n delete ctx.body.remember;\n\n // Store privateKey and remember in metadata for response interceptor\n ctx.metadata.privateKey = keyPair.privateKey;\n ctx.metadata.keyId = keyPair.keyId;\n ctx.metadata.algorithm = keyPair.algorithm;\n ctx.metadata.remember = remember;\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only process successful responses\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n // Handle both wrapped ({ data: { userId } }) and direct ({ userId }) responses\n const userData = ctx.response.body?.data || ctx.response.body;\n if (!userData?.userId)\n {\n authLogger.interceptor.login.error('No userId in response');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL (priority: runtime > global > env > default)\n const ttl = getSessionTtl(ctx.metadata.remember);\n\n // Encrypt session data\n const sessionData =\n {\n userId: userData.userId,\n privateKey: ctx.metadata.privateKey,\n keyId: ctx.metadata.keyId,\n algorithm: ctx.metadata.algorithm,\n };\n\n const sealed = await sealSession(sessionData, ttl);\n\n // Set HttpOnly session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Set keyId cookie (for oldKeyId lookup)\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.keyId,\n options: {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.login.error('Failed to save session', err);\n }\n\n await next();\n },\n };","/**\n * General Authentication Interceptor\n *\n * Handles authentication for all API requests except login/register\n * - Session validation and renewal\n * - JWT generation and signing\n * - Expired session cleanup\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs';\nimport { unsealSession, sealSession, shouldRefreshSession, generateClientToken, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\nimport { env } from '@spfn/core/config';\n\n/**\n * Check if path requires authentication\n */\nfunction requiresAuth(path: string): boolean\n{\n // Paths that don't require auth\n const publicPaths = [\n /^\\/_auth\\/login$/,\n /^\\/_auth\\/register$/,\n /^\\/_auth\\/codes$/, // Send verification code\n /^\\/_auth\\/codes\\/verify$/, // Verify code\n /^\\/_auth\\/exists$/, // Check account exists\n ];\n\n return !publicPaths.some((pattern) => pattern.test(path));\n}\n\n/**\n * General Authentication Interceptor\n *\n * Applies to all paths except login/register/codes\n * - Validates session\n * - Generates JWT token\n * - Refreshes session if needed\n * - Clears expired sessions\n */\nexport const generalAuthInterceptor: InterceptorRule =\n{\n pathPattern: '*', // Match all paths, filter by requiresAuth()\n method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n\n request: async (ctx, next) =>\n {\n // Skip if path doesn't require auth\n if (!requiresAuth(ctx.path))\n {\n authLogger.interceptor.general.debug(`Public path, skipping auth: ${ctx.path}`);\n await next();\n return;\n }\n\n // Log available cookies\n const cookieNames = Array.from(ctx.cookies.keys());\n authLogger.interceptor.general.debug('Available cookies:', {\n cookieNames,\n totalCount: cookieNames.length,\n lookingFor: COOKIE_NAMES.SESSION,\n });\n\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n authLogger.interceptor.general.debug('Request', {\n method: ctx.method,\n path: ctx.path,\n hasSession: !!sessionCookie,\n sessionCookieValue: sessionCookie ? '***EXISTS***' : 'NOT_FOUND',\n });\n\n // No session cookie\n if (!sessionCookie)\n {\n authLogger.interceptor.general.debug('No session cookie, proceeding without auth');\n // Let request proceed - server will return 401\n await next();\n return;\n }\n\n try\n {\n // Decrypt and validate session\n const session = await unsealSession(sessionCookie);\n\n authLogger.interceptor.general.debug('Session valid', {\n userId: session.userId,\n keyId: session.keyId,\n });\n\n // Check if session should be refreshed (within 24h of expiry)\n const needsRefresh = await shouldRefreshSession(sessionCookie, 24);\n\n if (needsRefresh)\n {\n authLogger.interceptor.general.debug('Session needs refresh (within 24h of expiry)');\n // Mark for session renewal in response interceptor\n ctx.metadata.refreshSession = true;\n ctx.metadata.sessionData = session;\n }\n\n // Generate JWT token\n const token = generateClientToken(\n {\n userId: session.userId,\n keyId: session.keyId,\n timestamp: Date.now(),\n },\n session.privateKey,\n session.algorithm,\n { expiresIn: '15m' }\n );\n\n authLogger.interceptor.general.debug('Generated JWT token (expires in 15m)');\n\n // Add authentication headers\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = session.keyId;\n\n // Store session info in metadata\n ctx.metadata.userId = session.userId;\n ctx.metadata.sessionValid = true;\n }\n catch (error)\n {\n const err = error as Error;\n\n // Session expired or invalid\n if (err.message.includes('expired') || err.message.includes('invalid'))\n {\n authLogger.interceptor.general.warn('Session expired or invalid', { message: err.message });\n authLogger.interceptor.general.debug('Marking session for cleanup');\n\n // Mark for cleanup in response interceptor\n ctx.metadata.clearSession = true;\n ctx.metadata.sessionValid = false;\n }\n else\n {\n authLogger.interceptor.general.error('Failed to process session', err);\n }\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Clear expired/invalid session\n if (ctx.metadata.clearSession)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n // Refresh session if needed and request was successful\n else if (ctx.metadata.refreshSession && ctx.response.status === 200)\n {\n try\n {\n const sessionData = ctx.metadata.sessionData;\n const ttl = getSessionTtl();\n\n // Re-encrypt session with new TTL\n const sealed = await sealSession(sessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: sessionData.keyId,\n options: {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n authLogger.interceptor.general.info('Session refreshed', { userId: sessionData.userId });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.general.error('Failed to refresh session', err);\n }\n }\n // Handle logout (clear session)\n else if (ctx.path === '/_auth/logout' && ctx.response.status === 200)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n\n await next();\n },\n};","/**\n * Key Rotation Interceptor\n *\n * Handles key rotation with new key generation and session update\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs';\nimport { generateKeyPair, unsealSession, sealSession, generateClientToken, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\n\n/**\n * Key Rotation Interceptor\n *\n * Request: Generates new key pair and adds to body, authenticates with current key\n * Response: Updates session with new privateKey\n */\nexport const keyRotationInterceptor: InterceptorRule =\n{\n pathPattern: '/_auth/keys/rotate',\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n await next();\n return;\n }\n\n try\n {\n // Get current session\n const currentSession = await unsealSession(sessionCookie);\n\n // Generate new key pair\n const newKeyPair = generateKeyPair('ES256');\n\n // Add new publicKey to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = newKeyPair.publicKey;\n ctx.body.keyId = newKeyPair.keyId;\n ctx.body.fingerprint = newKeyPair.fingerprint;\n ctx.body.algorithm = newKeyPair.algorithm;\n ctx.body.keySize = Buffer.from(newKeyPair.publicKey, 'base64').length;\n\n console.log('New key generated:', newKeyPair);\n console.log('publicKey:', newKeyPair.publicKey);\n console.log('keyId:', newKeyPair.keyId);\n console.log('fingerprint:', newKeyPair.fingerprint);\n\n // Authenticate with CURRENT key\n const token = generateClientToken(\n {\n userId: currentSession.userId,\n keyId: currentSession.keyId,\n action: 'rotate_key',\n timestamp: Date.now(),\n },\n currentSession.privateKey,\n currentSession.algorithm,\n {expiresIn: '15m'}\n );\n\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = currentSession.keyId;\n\n // Store new key and userId in metadata\n ctx.metadata.newPrivateKey = newKeyPair.privateKey;\n ctx.metadata.newKeyId = newKeyPair.keyId;\n ctx.metadata.newAlgorithm = newKeyPair.algorithm;\n ctx.metadata.userId = currentSession.userId;\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to prepare key rotation', err);\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only update session on successful rotation\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n if (!ctx.metadata.newPrivateKey || !ctx.metadata.userId)\n {\n authLogger.interceptor.keyRotation.error('Missing key rotation metadata');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL\n const ttl = getSessionTtl();\n\n // Create new session with rotated key\n const newSessionData =\n {\n userId: ctx.metadata.userId,\n privateKey: ctx.metadata.newPrivateKey,\n keyId: ctx.metadata.newKeyId,\n algorithm: ctx.metadata.newAlgorithm,\n };\n\n const sealed = await sealSession(newSessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.newKeyId,\n options: {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to update session after rotation', err);\n }\n\n await next();\n },\n};","/**\n * Auth Interceptors for Next.js Proxy\n *\n * Automatically registers interceptors for authentication flow\n *\n * Order matters - more specific interceptors first:\n * 1. loginRegisterInterceptor - Most specific (login/register only)\n * 2. keyRotationInterceptor - Specific (key rotation only)\n * 3. generalAuthInterceptor - General (all authenticated requests)\n */\n\nimport { loginRegisterInterceptor } from './login-register';\nimport { generalAuthInterceptor } from './general-auth';\nimport { keyRotationInterceptor } from './key-rotation';\n\n/**\n * All auth interceptors\n *\n * Execution order:\n * 1. loginRegisterInterceptor - Handles login/register (key generation + session save)\n * 2. keyRotationInterceptor - Handles key rotation (new key generation + session update)\n * 3. generalAuthInterceptor - Handles all authenticated requests (session validation + JWT injection + session renewal)\n */\nexport const authInterceptors = [\n loginRegisterInterceptor,\n keyRotationInterceptor,\n generalAuthInterceptor,\n];\n\nexport { loginRegisterInterceptor } from './login-register';\nexport { generalAuthInterceptor } from './general-auth';\nexport { keyRotationInterceptor } from './key-rotation';\n\n// Deprecated: use generalAuthInterceptor instead\nexport { generalAuthInterceptor as authenticationInterceptor };"],"mappings":"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","names":["sealSession","getSessionTtl","COOKIE_NAMES","authLogger","env","generateKeyPair","unsealSession","sealSession","generateClientToken","getSessionTtl","COOKIE_NAMES","authLogger"]}
|
|
1
|
+
{"version":3,"sources":["../../src/nextjs/api.ts","../../src/server/lib/crypto.ts","../../src/server/lib/session.ts","../../src/server/logger.ts","../../src/server/lib/config.ts","../../src/nextjs/interceptors/cookie-options.ts","../../src/nextjs/interceptors/login-register.ts","../../src/nextjs/interceptors/general-auth.ts","../../src/nextjs/interceptors/key-rotation.ts","../../src/server/lib/oauth/state.ts","../../src/nextjs/session-helpers.ts","../../src/nextjs/interceptors/oauth.ts","../../src/nextjs/interceptors/index.ts"],"sourcesContent":["/**\n * @spfn/auth/adapters/nextjs/api\n *\n * Next.js Adapter for SPFN Auth\n *\n * Provides automatic interceptor registration for seamless auth flow:\n * - Session management (HttpOnly cookies)\n * - JWT generation and signing\n * - Public key encryption\n *\n * @requires next >= 13.0.0\n *\n * @example\n * ```typescript\n * // Just import to auto-register interceptors\n * import '@spfn/auth/nextjs/api';\n * ```\n */\n\n// Re-export interceptors for advanced usage\nimport { registerInterceptors } from \"@spfn/core/nextjs/server\";\nimport { authInterceptors } from './interceptors';\n\n// Auto-register interceptors on import\nregisterInterceptors('auth', authInterceptors);","/**\n * @spfn/auth - Client Crypto Helpers\n *\n * ES256 (ECDSA P-256) key generation and JWT signing for Next.js\n * Keys are stored in DER format (Base64 encoded) for efficiency\n *\n * Key Sizes:\n * - ES256 (ECDSA P-256): ~91 bytes (Base64: ~120 chars)\n * - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)\n */\n\nimport { type KeyAlgorithmType } from '../types';\nimport crypto from 'crypto';\nimport jwt, { type Algorithm, type SignOptions } from 'jsonwebtoken';\n\ntype Unit =\n | \"Years\"\n | \"Year\"\n | \"Yrs\"\n | \"Yr\"\n | \"Y\"\n | \"Weeks\"\n | \"Week\"\n | \"W\"\n | \"Days\"\n | \"Day\"\n | \"D\"\n | \"Hours\"\n | \"Hour\"\n | \"Hrs\"\n | \"Hr\"\n | \"H\"\n | \"Minutes\"\n | \"Minute\"\n | \"Mins\"\n | \"Min\"\n | \"M\"\n | \"Seconds\"\n | \"Second\"\n | \"Secs\"\n | \"Sec\"\n | \"s\"\n | \"Milliseconds\"\n | \"Millisecond\"\n | \"Msecs\"\n | \"Msec\"\n | \"Ms\";\n\ntype UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;\n\ntype StringValue =\n | `${number}`\n | `${number}${UnitAnyCase}`\n | `${number} ${UnitAnyCase}`;\n\nexport interface KeyPair\n{\n privateKey: string; // Base64 encoded DER\n publicKey: string; // Base64 encoded DER\n keyId: string; // UUID\n fingerprint: string; // SHA-256 hash\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Generate ECDSA P-256 key pair (ES256)\n * Recommended for optimal size and performance\n */\nexport function generateKeyPairES256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {\n namedCurve: 'P-256', // ES256\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n // Convert Buffer to Base64\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n // Generate fingerprint (SHA-256 of public key)\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'ES256',\n };\n}\n\n/**\n * Generate RSA 2048 key pair (RS256)\n * Fallback option, larger size but wider compatibility\n */\nexport function generateKeyPairRS256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {\n modulusLength: 2048,\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'RS256',\n };\n}\n\n/**\n * Generate key pair (defaults to ES256)\n */\nexport function generateKeyPair(\n algorithm: KeyAlgorithmType = 'ES256'\n): KeyPair\n{\n return algorithm === 'ES256'\n ? generateKeyPairES256()\n : generateKeyPairRS256();\n}\n\n/**\n * Generate JWT signed with client private key (DER format)\n */\nexport function generateClientToken(\n payload: Record<string, any>,\n privateKeyB64: string,\n algorithm: Algorithm,\n options?: {\n expiresIn?: StringValue | number;\n issuer?: string;\n }\n): string\n{\n try\n {\n // Convert Base64 back to Buffer\n const privateKeyDER = Buffer.from(privateKeyB64, 'base64');\n\n // Create key object for signing\n const privateKeyObject = crypto.createPrivateKey({\n key: privateKeyDER,\n format: 'der',\n type: 'pkcs8',\n });\n\n // Export as PEM for jwt.sign\n const privateKeyPEM = privateKeyObject.export({\n type: 'pkcs8',\n format: 'pem',\n });\n\n const signOptions: SignOptions = {\n algorithm,\n issuer: options?.issuer || 'spfn-client',\n expiresIn: options?.expiresIn ?? '15m', // Default to 15 minutes\n }\n\n return jwt.sign(payload, privateKeyPEM, signOptions);\n }\n catch (error)\n {\n throw new Error(\n `Failed to generate client token: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n}\n\n/**\n * Get key size information\n */\nexport function getKeySize(publicKeyB64: string): {\n bytes: number;\n base64Length: number;\n}\n{\n const keyDER = Buffer.from(publicKeyB64, 'base64');\n\n return {\n bytes: keyDER.length,\n base64Length: publicKeyB64.length,\n };\n}\n\n/**\n * Check if key should be rotated based on creation date\n */\nexport function shouldRotateKey(\n createdAt: Date,\n rotationDays: number = 90\n): {\n shouldRotate: boolean;\n daysRemaining: number;\n}\n{\n const now = new Date();\n const ageInDays = Math.floor(\n (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)\n );\n const daysRemaining = Math.max(0, rotationDays - ageInDays);\n\n return {\n shouldRotate: daysRemaining <= 7, // Warn 7 days before expiry\n daysRemaining,\n };\n}","/**\n * @spfn/auth - Client Session Management\n *\n * Uses Jose JWE (JSON Web Encryption) to securely store session data in cookies\n * More efficient than Iron Session with better Edge Runtime support\n */\n\nimport * as jose from 'jose';\nimport { env } from '@spfn/auth/config';\nimport { env as coreEnv } from '@spfn/core/config';\nimport { authLogger } from '../logger';\n\nimport { type KeyAlgorithmType } from '../types';\n\nexport interface SessionData\n{\n userId: string;\n privateKey: string; // Base64 encoded DER\n keyId: string;\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Get session secret key derived from environment\n * Must be at least 32 characters (256-bit)\n *\n * Derives a 32-byte key using SHA-256 to ensure compatibility with Jose A256GCM\n */\nasync function getSessionSecretKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n\n // Derive a 32-byte key using SHA-256 for A256GCM compatibility\n // Use Web Crypto API for universal compatibility (browser + Node.js)\n const encoder = new TextEncoder();\n const data = encoder.encode(secret);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Get a short fingerprint of the current secret key for debugging\n * Logs only the first 8 hex chars of the SHA-256 hash — safe to expose\n */\nasync function getSecretFingerprint(): Promise<string>\n{\n const key = await getSessionSecretKey();\n const hash = await crypto.subtle.digest('SHA-256', key.buffer as ArrayBuffer);\n const hex = Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return hex.slice(0, 8);\n}\n\n/**\n * Seal session data into encrypted JWT (JWE)\n *\n * @param data - Session data to encrypt\n * @param ttl - Time to live in seconds (default: 7 days)\n * @returns Encrypted JWT string\n */\nexport async function sealSession(\n data: SessionData,\n ttl: number = 60 * 60 * 24 * 7 // 7 days\n): Promise<string>\n{\n const secret = await getSessionSecretKey();\n\n const result = await new jose.EncryptJWT({ data })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime(`${ttl}s`)\n .setIssuer('spfn-auth')\n .setAudience('spfn-client')\n .encrypt(secret);\n\n if (coreEnv.NODE_ENV !== 'production')\n {\n const fingerprint = await getSecretFingerprint();\n authLogger.session.debug(`Sealed session`, {\n secretFingerprint: fingerprint,\n resultLength: result.length,\n resultPrefix: result.slice(0, 20),\n });\n }\n\n return result;\n}\n\n/**\n * Unseal encrypted JWT (JWE) to session data\n *\n * @param jwt - Encrypted JWT string\n * @returns Session data\n * @throws Error if session is invalid or expired\n */\nexport async function unsealSession(jwt: string): Promise<SessionData>\n{\n try\n {\n const secret = await getSessionSecretKey();\n\n const { payload } = await jose.jwtDecrypt(jwt, secret, {\n issuer: 'spfn-auth',\n audience: 'spfn-client',\n });\n\n return payload.data as SessionData;\n }\n catch (err)\n {\n if (err instanceof jose.errors.JWTExpired)\n {\n throw new Error('Session expired');\n }\n\n if (err instanceof jose.errors.JWEDecryptionFailed)\n {\n // Log secret fingerprint for debugging cross-process key mismatch\n if (coreEnv.NODE_ENV !== 'production')\n {\n const fingerprint = await getSecretFingerprint();\n authLogger.session.warn(`JWE decryption failed`, {\n secretFingerprint: fingerprint,\n jwtLength: jwt.length,\n jwtPrefix: jwt.slice(0, 20),\n jwtSuffix: jwt.slice(-10),\n });\n }\n\n throw new Error('Invalid session');\n }\n\n if (err instanceof jose.errors.JWTClaimValidationFailed)\n {\n throw new Error('Session validation failed');\n }\n\n throw new Error('Failed to unseal session');\n }\n}\n\n/**\n * Get session metadata without decrypting\n *\n * @param jwt - Encrypted JWT string\n * @returns Session metadata or null if invalid\n */\nexport async function getSessionInfo(jwt: string): Promise<{\n issuedAt: Date;\n expiresAt: Date;\n issuer: string;\n audience: string;\n} | null>\n{\n const secret = await getSessionSecretKey();\n\n try\n {\n const { payload } = await jose.jwtDecrypt(jwt, secret);\n\n return {\n issuedAt: new Date(payload.iat! * 1000),\n expiresAt: new Date(payload.exp! * 1000),\n issuer: payload.iss || '',\n audience: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud || '',\n };\n }\n catch (err)\n {\n // Log error for debugging but return null for graceful handling\n if (coreEnv.NODE_ENV !== 'production')\n {\n authLogger.session.warn('Failed to get session info:', err instanceof Error ? err.message : 'Unknown error');\n }\n\n return null;\n }\n}\n\n/**\n * Check if session is about to expire (within threshold)\n *\n * @param jwt - Encrypted JWT string\n * @param thresholdHours - Hours before expiry to trigger refresh (default: 24)\n * @returns True if session should be refreshed\n */\nexport async function shouldRefreshSession(\n jwt: string,\n thresholdHours: number = 24\n): Promise<boolean>\n{\n const info = await getSessionInfo(jwt);\n\n if (!info)\n {\n return true;\n }\n\n const hoursRemaining = (info.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60);\n\n return hoursRemaining < thresholdHours;\n}","/**\n * @spfn/auth - Centralized Logger\n *\n * All auth package loggers with consistent naming\n */\n\nimport { logger as rootLogger } from '@spfn/core/logger';\n\nexport const authLogger = {\n plugin: rootLogger.child('@spfn/auth:plugin'),\n middleware: rootLogger.child('@spfn/auth:middleware'),\n interceptor: {\n general: rootLogger.child('@spfn/auth:interceptor:general'),\n login: rootLogger.child('@spfn/auth:interceptor:login'),\n keyRotation: rootLogger.child('@spfn/auth:interceptor:key-rotation'),\n oauth: rootLogger.child('@spfn/auth:interceptor:oauth'),\n },\n session: rootLogger.child('@spfn/auth:session'),\n service: rootLogger.child('@spfn/auth:service'),\n setup: rootLogger.child('@spfn/auth:setup'),\n email: rootLogger.child('@spfn/auth:email'),\n sms: rootLogger.child('@spfn/auth:sms'),\n};","/**\n * @spfn/auth - Global Configuration\n *\n * Manages global auth configuration including session TTL\n */\n\nimport { env } from '@spfn/auth/config';\n\n/**\n * Cookie name suffix derived from PORT to isolate sessions across\n * multiple local dev instances running on the same domain (localhost).\n */\nfunction getCookieSuffix(): string\n{\n const port = process.env.PORT;\n return port ? `_${port}` : '';\n}\n\n/**\n * Cookie names used by SPFN Auth\n *\n * Names include a port-based suffix so that multiple dev instances\n * on different ports don't overwrite each other's cookies.\n */\nexport const COOKIE_NAMES = {\n /** Encrypted session data (userId, privateKey, keyId, algorithm) */\n get SESSION() { return `spfn_session${getCookieSuffix()}`; },\n /** Current key ID (for key rotation) */\n get SESSION_KEY_ID() { return `spfn_session_key_id${getCookieSuffix()}`; },\n /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */\n get OAUTH_PENDING() { return `spfn_oauth_pending${getCookieSuffix()}`; },\n};\n\n/**\n * Parse duration string to seconds\n *\n * Supports: '30d', '12h', '45m', '3600s', or plain number\n *\n * @example\n * parseDuration('30d') // 2592000 (30 days in seconds)\n * parseDuration('12h') // 43200\n * parseDuration('45m') // 2700\n * parseDuration('3600') // 3600\n */\nexport function parseDuration(duration: string | number): number\n{\n if (typeof duration === 'number')\n {\n return duration;\n }\n\n const match = duration.match(/^(\\d+)([dhms]?)$/);\n if (!match)\n {\n throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2] || 's';\n\n switch (unit)\n {\n case 'd':\n return value * 24 * 60 * 60;\n case 'h':\n return value * 60 * 60;\n case 'm':\n return value * 60;\n case 's':\n return value;\n default:\n throw new Error(`Unknown duration unit: ${unit}`);\n }\n}\n\n/**\n * Auth configuration\n */\nexport interface AuthConfig\n{\n /**\n * Default session TTL in seconds or duration string\n *\n * Supports:\n * - Number: seconds (e.g., 2592000)\n * - String: '30d', '12h', '45m', '3600s'\n *\n * @default 7d (7 days)\n */\n sessionTtl?: string | number;\n}\n\n/**\n * Global auth configuration state\n */\nlet globalConfig: AuthConfig = {\n sessionTtl: '7d', // Default: 7 days\n};\n\n/**\n * Configure global auth settings\n *\n * @param config - Auth configuration\n *\n * @example\n * ```typescript\n * configureAuth({\n * sessionTtl: '30d', // 30 days\n * });\n * ```\n */\nexport function configureAuth(config: AuthConfig): void\n{\n globalConfig = {\n ...globalConfig,\n ...config,\n };\n}\n\n/**\n * Get current auth configuration\n */\nexport function getAuthConfig(): AuthConfig\n{\n return { ...globalConfig };\n}\n\n/**\n * Get session TTL in seconds\n *\n * Priority:\n * 1. Runtime override (remember parameter)\n * 2. Global config (configureAuth)\n * 3. Environment variable (SPFN_AUTH_SESSION_TTL) - via config module\n * 4. Default (7 days)\n */\nexport function getSessionTtl(override?: string | number): number\n{\n // 1. Runtime override\n if (override !== undefined)\n {\n return parseDuration(override);\n }\n\n // 2. Global config\n if (globalConfig.sessionTtl !== undefined)\n {\n return parseDuration(globalConfig.sessionTtl);\n }\n\n // 3. Environment variable (from config module)\n const envTtl = env.SPFN_AUTH_SESSION_TTL;\n if (envTtl)\n {\n return parseDuration(envTtl);\n }\n\n // 4. Default: 7 days\n return 7 * 24 * 60 * 60;\n}","/**\n * Shared cookie option helpers for auth interceptors\n *\n * SPFN_AUTH_COOKIE_SECURE env var allows overriding the Secure flag.\n * - unset: defaults to NODE_ENV === 'production'\n * - \"true\" / \"false\": explicit override\n *\n * Useful for HTTP-only staging environments (e.g. bastion over plain HTTP).\n */\n\n/**\n * Resolve whether cookies should have the Secure flag.\n *\n * Priority:\n * 1. SPFN_AUTH_COOKIE_SECURE (explicit override)\n * 2. NODE_ENV === 'production'\n */\nfunction resolveSecure(): boolean\n{\n const override = process.env.SPFN_AUTH_COOKIE_SECURE;\n\n if (override !== undefined)\n {\n return override === 'true';\n }\n\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Whether cookies should have the Secure flag.\n * Evaluated once at module load time.\n */\nexport const cookieSecure = resolveSecure();\n","/**\n * Login/Register Interceptor\n *\n * Automatically handles key generation and session management\n * for login and register endpoints\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair } from '../../server/lib/crypto';\nimport { sealSession } from '../../server/lib/session';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Login and Register Interceptor\n *\n * Request: Generates key pair and adds publicKey to request body\n * Response: Saves privateKey to HttpOnly cookie\n */\nexport const loginRegisterInterceptor: InterceptorRule =\n {\n pathPattern: /^\\/_auth\\/(login|register)$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n // Get old session if exists (for key rotation on login)\n const oldKeyId = ctx.cookies.get(COOKIE_NAMES.SESSION_KEY_ID);\n\n // Extract remember option from request body (if provided)\n const remember = ctx.body?.remember;\n\n // Generate new key pair\n const keyPair = generateKeyPair('ES256');\n\n // Add publicKey data to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = keyPair.publicKey;\n ctx.body.keyId = keyPair.keyId;\n ctx.body.fingerprint = keyPair.fingerprint;\n ctx.body.algorithm = keyPair.algorithm;\n ctx.body.keySize = Buffer.from(keyPair.publicKey, 'base64').length;\n\n // Add oldKeyId for login (key rotation)\n if (ctx.path === '/_auth/login' && oldKeyId)\n {\n ctx.body.oldKeyId = oldKeyId;\n }\n\n // Remove remember from body (not part of contract)\n delete ctx.body.remember;\n\n // Store privateKey and remember in metadata for response interceptor\n ctx.metadata.privateKey = keyPair.privateKey;\n ctx.metadata.keyId = keyPair.keyId;\n ctx.metadata.algorithm = keyPair.algorithm;\n ctx.metadata.remember = remember;\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only process successful responses\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n // Handle both wrapped ({ data: { userId } }) and direct ({ userId }) responses\n const userData = ctx.response.body?.data || ctx.response.body;\n if (!userData?.userId)\n {\n authLogger.interceptor.login.error('No userId in response');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL (priority: runtime > global > env > default)\n const ttl = getSessionTtl(ctx.metadata.remember);\n\n // Encrypt session data\n const sessionData =\n {\n userId: userData.userId,\n privateKey: ctx.metadata.privateKey,\n keyId: ctx.metadata.keyId,\n algorithm: ctx.metadata.algorithm,\n };\n\n const sealed = await sealSession(sessionData, ttl);\n\n // Set HttpOnly session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Set keyId cookie (for oldKeyId lookup)\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.login.error('Failed to save session', err);\n }\n\n await next();\n },\n };","/**\n * General Authentication Interceptor\n *\n * Handles authentication for all API requests except login/register\n * - Session validation and renewal\n * - JWT generation and signing\n * - Expired session cleanup\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { unsealSession, sealSession, shouldRefreshSession } from '../../server/lib/session';\nimport { generateClientToken } from '../../server/lib/crypto';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Check if path requires authentication\n */\nfunction requiresAuth(path: string): boolean\n{\n // Paths that don't require auth\n const publicPaths = [\n /^\\/_auth\\/login$/,\n /^\\/_auth\\/register$/,\n /^\\/_auth\\/codes$/, // Send verification code\n /^\\/_auth\\/codes\\/verify$/, // Verify code\n /^\\/_auth\\/exists$/, // Check account exists\n ];\n\n return !publicPaths.some((pattern) => pattern.test(path));\n}\n\n/**\n * General Authentication Interceptor\n *\n * Applies to all paths except login/register/codes\n * - Validates session\n * - Generates JWT token\n * - Refreshes session if needed\n * - Clears expired sessions\n */\nexport const generalAuthInterceptor: InterceptorRule =\n{\n pathPattern: '*', // Match all paths, filter by requiresAuth()\n method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n\n request: async (ctx, next) =>\n {\n // Skip if path doesn't require auth\n if (!requiresAuth(ctx.path))\n {\n authLogger.interceptor.general.debug(`Public path, skipping auth: ${ctx.path}`);\n await next();\n return;\n }\n\n // Log available cookies\n const cookieNames = Array.from(ctx.cookies.keys());\n authLogger.interceptor.general.debug('Available cookies:', {\n cookieNames,\n totalCount: cookieNames.length,\n lookingFor: COOKIE_NAMES.SESSION,\n });\n\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n authLogger.interceptor.general.debug('Request', {\n method: ctx.method,\n path: ctx.path,\n hasSession: !!sessionCookie,\n sessionLength: sessionCookie?.length ?? 0,\n sessionPrefix: sessionCookie?.slice(0, 20) ?? '',\n sessionSuffix: sessionCookie?.slice(-10) ?? '',\n });\n\n // No session cookie\n if (!sessionCookie)\n {\n authLogger.interceptor.general.debug('No session cookie, proceeding without auth');\n // Let request proceed - server will return 401\n await next();\n return;\n }\n\n try\n {\n // Decrypt and validate session\n const session = await unsealSession(sessionCookie);\n\n authLogger.interceptor.general.debug('Session valid', {\n userId: session.userId,\n keyId: session.keyId,\n });\n\n // Check if session should be refreshed (within 24h of expiry)\n const needsRefresh = await shouldRefreshSession(sessionCookie, 24);\n\n if (needsRefresh)\n {\n authLogger.interceptor.general.debug('Session needs refresh (within 24h of expiry)');\n // Mark for session renewal in response interceptor\n ctx.metadata.refreshSession = true;\n ctx.metadata.sessionData = session;\n }\n\n // Generate JWT token\n const token = generateClientToken(\n {\n userId: session.userId,\n keyId: session.keyId,\n timestamp: Date.now(),\n },\n session.privateKey,\n session.algorithm,\n { expiresIn: '15m' }\n );\n\n authLogger.interceptor.general.debug('Generated JWT token (expires in 15m)');\n\n // Add authentication headers\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = session.keyId;\n\n // Store session info in metadata\n ctx.metadata.userId = session.userId;\n ctx.metadata.sessionValid = true;\n }\n catch (error)\n {\n const err = error as Error;\n const msg = err.message.toLowerCase();\n\n // Session expired or invalid\n if (msg.includes('expired') || msg.includes('invalid'))\n {\n authLogger.interceptor.general.warn('Session expired or invalid', {\n message: err.message,\n cookieLength: sessionCookie.length,\n cookiePrefix: sessionCookie.slice(0, 20),\n cookieSuffix: sessionCookie.slice(-10),\n });\n authLogger.interceptor.general.debug('Marking session for cleanup');\n\n // Mark for cleanup in response interceptor\n ctx.metadata.clearSession = true;\n ctx.metadata.sessionValid = false;\n }\n else\n {\n authLogger.interceptor.general.error('Failed to process session', err);\n }\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Backend returned 401 with a valid session — server rejected it\n if (ctx.response.status === 401 && ctx.metadata.sessionValid)\n {\n authLogger.interceptor.general.warn('Backend returned 401, clearing session');\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n await next();\n return;\n }\n\n // Clear expired/invalid session\n if (ctx.metadata.clearSession)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n // Refresh session if needed and request was successful\n else if (ctx.metadata.refreshSession && ctx.response.status === 200)\n {\n try\n {\n const sessionData = ctx.metadata.sessionData;\n const ttl = getSessionTtl();\n\n // Re-encrypt session with new TTL\n const sealed = await sealSession(sessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: sessionData.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n authLogger.interceptor.general.info('Session refreshed', {\n userId: sessionData.userId,\n sealedLength: sealed.length,\n sealedPrefix: sealed.slice(0, 20),\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.general.error('Failed to refresh session', err);\n }\n }\n // Handle logout (clear session)\n else if (ctx.path === '/_auth/logout' && ctx.response.ok)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n\n await next();\n },\n};","/**\n * Key Rotation Interceptor\n *\n * Handles key rotation with new key generation and session update\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair, generateClientToken } from '../../server/lib/crypto';\nimport { unsealSession, sealSession } from '../../server/lib/session';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Key Rotation Interceptor\n *\n * Request: Generates new key pair and adds to body, authenticates with current key\n * Response: Updates session with new privateKey\n */\nexport const keyRotationInterceptor: InterceptorRule =\n{\n pathPattern: '/_auth/keys/rotate',\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n await next();\n return;\n }\n\n try\n {\n // Get current session\n const currentSession = await unsealSession(sessionCookie);\n\n // Generate new key pair\n const newKeyPair = generateKeyPair('ES256');\n\n // Add new publicKey to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = newKeyPair.publicKey;\n ctx.body.keyId = newKeyPair.keyId;\n ctx.body.fingerprint = newKeyPair.fingerprint;\n ctx.body.algorithm = newKeyPair.algorithm;\n ctx.body.keySize = Buffer.from(newKeyPair.publicKey, 'base64').length;\n\n console.log('New key generated:', newKeyPair);\n console.log('publicKey:', newKeyPair.publicKey);\n console.log('keyId:', newKeyPair.keyId);\n console.log('fingerprint:', newKeyPair.fingerprint);\n\n // Authenticate with CURRENT key\n const token = generateClientToken(\n {\n userId: currentSession.userId,\n keyId: currentSession.keyId,\n action: 'rotate_key',\n timestamp: Date.now(),\n },\n currentSession.privateKey,\n currentSession.algorithm,\n {expiresIn: '15m'}\n );\n\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = currentSession.keyId;\n\n // Store new key and userId in metadata\n ctx.metadata.newPrivateKey = newKeyPair.privateKey;\n ctx.metadata.newKeyId = newKeyPair.keyId;\n ctx.metadata.newAlgorithm = newKeyPair.algorithm;\n ctx.metadata.userId = currentSession.userId;\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to prepare key rotation', err);\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only update session on successful rotation\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n if (!ctx.metadata.newPrivateKey || !ctx.metadata.userId)\n {\n authLogger.interceptor.keyRotation.error('Missing key rotation metadata');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL\n const ttl = getSessionTtl();\n\n // Create new session with rotated key\n const newSessionData =\n {\n userId: ctx.metadata.userId,\n privateKey: ctx.metadata.newPrivateKey,\n keyId: ctx.metadata.newKeyId,\n algorithm: ctx.metadata.newAlgorithm,\n };\n\n const sealed = await sealSession(newSessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.newKeyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to update session after rotation', err);\n }\n\n await next();\n },\n};","/**\n * OAuth State Management\n *\n * CSRF 방지를 위한 state 파라미터 암호화/복호화\n * - returnUrl: OAuth 성공 후 리다이렉트할 URL\n * - nonce: CSRF 방지용 일회용 토큰\n * - provider: OAuth provider (google, github 등)\n * - publicKey, keyId, fingerprint, algorithm: 클라이언트 키 정보\n * - expiresAt: state 만료 시간\n */\n\nimport * as jose from 'jose';\nimport { env } from '@spfn/auth/config';\nimport { type KeyAlgorithmType } from '../../types';\n\nexport interface OAuthState\n{\n returnUrl: string;\n nonce: string;\n provider: string;\n publicKey: string;\n keyId: string;\n fingerprint: string;\n algorithm: KeyAlgorithmType;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Get encryption key derived from session secret\n */\nasync function getStateKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n const encoder = new TextEncoder();\n const data = encoder.encode(`oauth-state:${secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Generate random nonce\n */\nfunction generateNonce(): string\n{\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');\n}\n\nexport interface CreateOAuthStateParams\n{\n provider: string;\n returnUrl: string;\n publicKey: string;\n keyId: string;\n fingerprint: string;\n algorithm: KeyAlgorithmType;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * OAuth state 생성 및 암호화\n *\n * @param params - state 생성에 필요한 파라미터\n * @returns 암호화된 state 문자열\n */\nexport async function createOAuthState(params: CreateOAuthStateParams): Promise<string>\n{\n const key = await getStateKey();\n\n const state: OAuthState = {\n returnUrl: params.returnUrl,\n nonce: generateNonce(),\n provider: params.provider,\n publicKey: params.publicKey,\n keyId: params.keyId,\n fingerprint: params.fingerprint,\n algorithm: params.algorithm,\n metadata: params.metadata,\n };\n\n const jwe = await new jose.EncryptJWT({ state })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime('10m')\n .encrypt(key);\n\n // URL-safe base64 encoding\n return encodeURIComponent(jwe);\n}\n\n/**\n * OAuth state 복호화 및 검증\n *\n * @param encryptedState - 암호화된 state 문자열\n * @returns 복호화된 state 객체\n * @throws Error if state is invalid or expired (JWE exp claim으로 자동 검증)\n */\nexport async function verifyOAuthState(encryptedState: string): Promise<OAuthState>\n{\n const key = await getStateKey();\n\n const jwe = decodeURIComponent(encryptedState);\n const { payload } = await jose.jwtDecrypt(jwe, key);\n\n return payload.state as OAuthState;\n}\n","/**\n * Session helpers for Next.js\n *\n * Server-side only (uses next/headers)\n */\n\nimport * as jose from 'jose';\nimport { cookies } from 'next/headers.js';\nimport { sealSession, unsealSession, type SessionData } from '../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl, parseDuration } from '../server/lib/config';\nimport { type KeyAlgorithmType } from '../server/types';\nimport { env } from '@spfn/auth/config';\nimport { logger } from '@spfn/core/logger';\n\nexport type { SessionData };\n\n/**\n * Pending OAuth session data (before user ID is known)\n */\nexport interface PendingSessionData\n{\n privateKey: string;\n keyId: string;\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Public session information (excludes sensitive data)\n */\nexport interface PublicSession\n{\n /** User ID */\n userId: string;\n}\n\n/**\n * Options for saveSession\n */\nexport interface SaveSessionOptions\n{\n /**\n * Session TTL (time to live)\n *\n * Supports:\n * - Number: seconds (e.g., 2592000)\n * - String: duration format ('30d', '12h', '45m', '3600s')\n *\n * If not provided, uses global configuration:\n * 1. Global config (configureAuth)\n * 2. Environment variable (SPFN_AUTH_SESSION_TTL)\n * 3. Default (7d)\n */\n maxAge?: number | string;\n\n /**\n * Remember me option\n *\n * When true, uses extended session duration (if configured)\n */\n remember?: boolean;\n}\n\n/**\n * Save session to HttpOnly cookie\n *\n * @param data - Session data to save\n * @param options - Session options (maxAge, remember)\n *\n * @example\n * ```typescript\n * // Use global configuration\n * await saveSession(sessionData);\n *\n * // Custom TTL with duration string\n * await saveSession(sessionData, { maxAge: '30d' });\n *\n * // Custom TTL in seconds\n * await saveSession(sessionData, { maxAge: 2592000 });\n *\n * // Remember me\n * await saveSession(sessionData, { remember: true });\n * ```\n */\nexport async function saveSession(\n data: SessionData,\n options?: SaveSessionOptions\n): Promise<void>\n{\n // Calculate maxAge\n let maxAge: number;\n\n if (options?.maxAge !== undefined)\n {\n // Custom maxAge provided\n maxAge = typeof options.maxAge === 'number'\n ? options.maxAge\n : parseDuration(options.maxAge);\n }\n else\n {\n // Use getSessionTtl for consistent configuration\n maxAge = getSessionTtl();\n }\n\n const token = await sealSession(data, maxAge);\n const cookieStore = await cookies();\n\n cookieStore.set(COOKIE_NAMES.SESSION, token, {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n path: '/',\n maxAge\n });\n}\n\n/**\n * Get session from HttpOnly cookie\n *\n * Returns public session info only (excludes privateKey, algorithm, keyId)\n */\nexport async function getSession(): Promise<PublicSession | null>\n{\n const cookieStore = await cookies();\n const sessionCookie = cookieStore.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n return null;\n }\n\n try\n {\n logger.debug('Validating session cookie', { cookie: sessionCookie.value });\n const session = await unsealSession(sessionCookie.value);\n // Return only public information\n return {\n userId: session.userId,\n };\n }\n catch (error)\n {\n // Session expired or invalid\n // Note: Cannot delete cookies in Server Components (read-only)\n // Use validateSessionMiddleware() in Next.js middleware for automatic cleanup\n logger.debug('Session validation failed', {\n error: error instanceof Error ? error.message : String(error)\n });\n\n return null;\n }\n}\n\n/**\n * Clear session cookie\n */\nexport async function clearSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.SESSION);\n cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);\n}\n\n// ============================================================================\n// Pending OAuth Session (for OAuth flow)\n// ============================================================================\n\n/**\n * Get encryption key for pending session\n */\nasync function getPendingSessionKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n const encoder = new TextEncoder();\n const data = encoder.encode(`oauth-pending:${secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Seal pending session data (for OAuth flow)\n *\n * @param data - Pending session data (privateKey, keyId, algorithm)\n * @param ttl - Time to live in seconds (default: 10 minutes)\n */\nexport async function sealPendingSession(\n data: PendingSessionData,\n ttl: number = 600\n): Promise<string>\n{\n const key = await getPendingSessionKey();\n\n return await new jose.EncryptJWT({ data })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime(`${ttl}s`)\n .setIssuer('spfn-auth')\n .setAudience('spfn-oauth')\n .encrypt(key);\n}\n\n/**\n * Unseal pending session data\n *\n * @param jwt - Encrypted pending session token\n */\nexport async function unsealPendingSession(jwt: string): Promise<PendingSessionData>\n{\n const key = await getPendingSessionKey();\n\n const { payload } = await jose.jwtDecrypt(jwt, key, {\n issuer: 'spfn-auth',\n audience: 'spfn-oauth',\n });\n\n return payload.data as PendingSessionData;\n}\n\n/**\n * Get pending session from cookie\n */\nexport async function getPendingSession(): Promise<PendingSessionData | null>\n{\n const cookieStore = await cookies();\n const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);\n\n if (!pendingCookie)\n {\n return null;\n }\n\n try\n {\n return await unsealPendingSession(pendingCookie.value);\n }\n catch (error)\n {\n logger.debug('Pending session validation failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n}\n\n/**\n * Clear pending session cookie\n */\nexport async function clearPendingSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.OAUTH_PENDING);\n}\n","/**\n * OAuth Interceptors\n *\n * 1. oauthUrlInterceptor: OAuth URL 요청 시 키쌍 생성 및 state 주입\n * 2. oauthFinalizeInterceptor: OAuth 완료 시 pending session에서 세션 저장\n */\n\nimport type { InterceptorRule, ResponseInterceptorContext } from '@spfn/core/nextjs/server';\nimport { generateKeyPair } from '../../server/lib/crypto';\nimport { createOAuthState } from '../../server/lib/oauth/state';\nimport { sealSession } from '../../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { sealPendingSession, unsealPendingSession } from '../session-helpers';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * OAuth URL Interceptor\n *\n * POST /_auth/oauth/:provider/url 요청을 가로채서\n * 키쌍 생성 및 state 주입 처리\n */\nexport const oauthUrlInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/\\w+\\/url$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const provider = ctx.path.split('/')[3]; // google, github, etc.\n const returnUrl = ctx.body?.returnUrl || '/';\n\n // 키쌍 생성\n const keyPair = generateKeyPair('ES256');\n\n // state 생성 (publicKey 포함)\n const state = await createOAuthState({\n provider,\n returnUrl,\n publicKey: keyPair.publicKey,\n keyId: keyPair.keyId,\n fingerprint: keyPair.fingerprint,\n algorithm: keyPair.algorithm,\n });\n\n // body에 state 주입\n if (!ctx.body)\n {\n ctx.body = {};\n }\n ctx.body.state = state;\n\n // pending session 저장용 metadata\n ctx.metadata.pendingSession = {\n privateKey: keyPair.privateKey,\n keyId: keyPair.keyId,\n algorithm: keyPair.algorithm,\n };\n\n authLogger.interceptor.oauth?.debug?.('OAuth state created', {\n provider,\n keyId: keyPair.keyId,\n });\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // 성공 응답이고 pending session이 있으면 쿠키 설정\n if (ctx.response.ok && ctx.metadata.pendingSession)\n {\n try\n {\n const sealed = await sealPendingSession(ctx.metadata.pendingSession);\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax', // OAuth 리다이렉트 허용\n maxAge: 600, // 10분\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('Pending session cookie set', {\n keyId: ctx.metadata.pendingSession.keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to set pending session', err);\n }\n }\n\n await next();\n },\n};\n\n/**\n * Finalize 실패 시 에러 응답 설정 + pending 쿠키 정리\n */\nfunction setFinalizeError(ctx: ResponseInterceptorContext, message: string): void\n{\n ctx.response.ok = false;\n ctx.response.status = 401;\n ctx.response.statusText = 'Unauthorized';\n ctx.response.body = { success: false, message };\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n}\n\n/**\n * OAuth Finalize Interceptor\n *\n * POST /_auth/oauth/finalize 요청을 가로채서\n * pending session에서 세션 저장\n */\nexport const oauthFinalizeInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/finalize$/,\n method: 'POST',\n\n response: async (ctx, next) =>\n {\n // 성공 응답일 때만 처리\n if (!ctx.response.ok)\n {\n await next();\n return;\n }\n\n const pendingCookie = ctx.cookies.get(COOKIE_NAMES.OAUTH_PENDING);\n if (!pendingCookie)\n {\n authLogger.interceptor.oauth?.warn?.('No pending session cookie found');\n setFinalizeError(ctx, 'OAuth session expired. Please try again.');\n await next();\n return;\n }\n\n try\n {\n // pending session에서 privateKey 복원\n const pendingSession = await unsealPendingSession(pendingCookie);\n\n // body에서 userId, keyId 추출\n const { userId, keyId } = ctx.response.body || {};\n\n if (!userId || !keyId)\n {\n authLogger.interceptor.oauth?.error?.('Missing userId or keyId in response');\n setFinalizeError(ctx, 'OAuth finalize failed: missing credentials');\n await next();\n return;\n }\n\n // keyId 일치 확인\n if (pendingSession.keyId !== keyId)\n {\n authLogger.interceptor.oauth?.error?.('KeyId mismatch', {\n expected: pendingSession.keyId,\n received: keyId,\n });\n setFinalizeError(ctx, 'OAuth session mismatch. Please try again.');\n await next();\n return;\n }\n\n // 세션 생성\n const ttl = getSessionTtl();\n const sessionToken = await sealSession({\n userId,\n privateKey: pendingSession.privateKey,\n keyId: pendingSession.keyId,\n algorithm: pendingSession.algorithm,\n }, ttl);\n\n // 세션 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sessionToken,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // keyId 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // pending session 쿠키 삭제 (maxAge: 0)\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('OAuth session finalized', {\n userId,\n keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to finalize OAuth session', err);\n setFinalizeError(ctx, err.message);\n }\n\n await next();\n },\n};\n","/**\n * Auth Interceptors for Next.js Proxy\n *\n * Automatically registers interceptors for authentication flow\n *\n * Order matters - more specific interceptors first:\n * 1. loginRegisterInterceptor - Most specific (login/register only)\n * 2. keyRotationInterceptor - Specific (key rotation only)\n * 3. oauthUrlInterceptor - OAuth URL generation (key generation + state injection)\n * 4. generalAuthInterceptor - General (all authenticated requests)\n */\n\nimport { loginRegisterInterceptor } from './login-register';\nimport { generalAuthInterceptor } from './general-auth';\nimport { keyRotationInterceptor } from './key-rotation';\nimport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n/**\n * All auth interceptors\n *\n * Execution order:\n * 1. loginRegisterInterceptor - Handles login/register (key generation + session save)\n * 2. keyRotationInterceptor - Handles key rotation (new key generation + session update)\n * 3. oauthUrlInterceptor - Handles OAuth URL requests (key generation + state injection + pending session)\n * 4. oauthFinalizeInterceptor - Handles OAuth finalize (pending session → full session)\n * 5. generalAuthInterceptor - Handles all authenticated requests (session validation + JWT injection + session renewal)\n */\nexport const authInterceptors = [\n loginRegisterInterceptor,\n keyRotationInterceptor,\n oauthUrlInterceptor,\n oauthFinalizeInterceptor,\n generalAuthInterceptor,\n];\n\nexport { loginRegisterInterceptor } from './login-register';\nexport { generalAuthInterceptor } from './general-auth';\nexport { keyRotationInterceptor } from './key-rotation';\nexport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n// Deprecated: use generalAuthInterceptor instead\nexport { generalAuthInterceptor as authenticationInterceptor };"],"mappings":"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","names":["crypto","jwt","env","env","jose","env","jose","env","env","jwt"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface OAuthCallbackProps {
|
|
4
|
+
/**
|
|
5
|
+
* API base path for RPC calls
|
|
6
|
+
* @default '/api/rpc'
|
|
7
|
+
*/
|
|
8
|
+
apiBasePath?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Custom loading component
|
|
11
|
+
*/
|
|
12
|
+
loadingComponent?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Custom error component
|
|
15
|
+
*/
|
|
16
|
+
errorComponent?: (error: string) => React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Callback after successful OAuth
|
|
19
|
+
*/
|
|
20
|
+
onSuccess?: (userId: string) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Callback on error
|
|
23
|
+
*/
|
|
24
|
+
onError?: (error: string) => void;
|
|
25
|
+
}
|
|
26
|
+
declare function OAuthCallback({ apiBasePath, loadingComponent, errorComponent, onSuccess, onError, }: OAuthCallbackProps): react_jsx_runtime.JSX.Element | null;
|
|
27
|
+
|
|
28
|
+
export { OAuthCallback, type OAuthCallbackProps };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/nextjs/components/oauth-callback.tsx
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function OAuthCallback({
|
|
7
|
+
apiBasePath = "/api/rpc",
|
|
8
|
+
loadingComponent,
|
|
9
|
+
errorComponent,
|
|
10
|
+
onSuccess,
|
|
11
|
+
onError
|
|
12
|
+
}) {
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
async function finalizeOAuth() {
|
|
17
|
+
try {
|
|
18
|
+
const params = new URLSearchParams(window.location.search);
|
|
19
|
+
const userId = params.get("userId");
|
|
20
|
+
const keyId = params.get("keyId");
|
|
21
|
+
const returnUrl = params.get("returnUrl") || "/";
|
|
22
|
+
const errorParam = params.get("error");
|
|
23
|
+
if (errorParam) {
|
|
24
|
+
throw new Error(errorParam);
|
|
25
|
+
}
|
|
26
|
+
if (!userId || !keyId) {
|
|
27
|
+
throw new Error("Missing required parameters");
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(`${apiBasePath}/oauthFinalize`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json"
|
|
33
|
+
},
|
|
34
|
+
credentials: "include",
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
body: {
|
|
37
|
+
userId,
|
|
38
|
+
keyId,
|
|
39
|
+
returnUrl
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const data2 = await response.json().catch(() => ({}));
|
|
45
|
+
throw new Error(data2.message || "Failed to finalize OAuth");
|
|
46
|
+
}
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
onSuccess?.(userId);
|
|
49
|
+
window.location.href = data.returnUrl || returnUrl;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const message = err instanceof Error ? err.message : "OAuth failed";
|
|
52
|
+
setError(message);
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
onError?.(message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finalizeOAuth();
|
|
58
|
+
}, [apiBasePath, onSuccess, onError]);
|
|
59
|
+
if (error) {
|
|
60
|
+
if (errorComponent) {
|
|
61
|
+
return /* @__PURE__ */ jsx(Fragment, { children: errorComponent(error) });
|
|
62
|
+
}
|
|
63
|
+
return /* @__PURE__ */ jsxs("div", { style: { padding: "20px", textAlign: "center" }, children: [
|
|
64
|
+
/* @__PURE__ */ jsx("h2", { children: "Authentication Error" }),
|
|
65
|
+
/* @__PURE__ */ jsx("p", { style: { color: "red" }, children: error }),
|
|
66
|
+
/* @__PURE__ */ jsx("button", { onClick: () => window.location.href = "/", children: "Go Home" })
|
|
67
|
+
] });
|
|
68
|
+
}
|
|
69
|
+
if (isLoading) {
|
|
70
|
+
if (loadingComponent) {
|
|
71
|
+
return /* @__PURE__ */ jsx(Fragment, { children: loadingComponent });
|
|
72
|
+
}
|
|
73
|
+
return /* @__PURE__ */ jsx("div", { style: { padding: "20px", textAlign: "center" }, children: /* @__PURE__ */ jsx("p", { children: "Completing authentication..." }) });
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
OAuthCallback
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/nextjs/components/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n/**\n * OAuthCallback Component\n *\n * OAuth 콜백 페이지용 클라이언트 컴포넌트\n * URL params에서 userId, keyId를 추출하여 oauthFinalize API 호출 후 returnUrl로 리다이렉트\n *\n * @example\n * ```tsx\n * // app/auth/callback/page.tsx\n * export { OAuthCallback as default } from '@spfn/auth/nextjs/client';\n * ```\n */\n\nimport { useEffect, useState } from 'react';\n\nexport interface OAuthCallbackProps\n{\n /**\n * API base path for RPC calls\n * @default '/api/rpc'\n */\n apiBasePath?: string;\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode;\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode;\n\n /**\n * Callback after successful OAuth\n */\n onSuccess?: (userId: string) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: string) => void;\n}\n\nexport function OAuthCallback({\n apiBasePath = '/api/rpc',\n loadingComponent,\n errorComponent,\n onSuccess,\n onError,\n}: OAuthCallbackProps)\n{\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() =>\n {\n async function finalizeOAuth()\n {\n try\n {\n const params = new URLSearchParams(window.location.search);\n const userId = params.get('userId');\n const keyId = params.get('keyId');\n const returnUrl = params.get('returnUrl') || '/';\n const errorParam = params.get('error');\n\n // Handle error from backend\n if (errorParam)\n {\n throw new Error(errorParam);\n }\n\n if (!userId || !keyId)\n {\n throw new Error('Missing required parameters');\n }\n\n // Call oauthFinalize API\n const response = await fetch(`${apiBasePath}/oauthFinalize`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n credentials: 'include',\n body: JSON.stringify({\n body: {\n userId,\n keyId,\n returnUrl,\n },\n }),\n });\n\n if (!response.ok)\n {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.message || 'Failed to finalize OAuth');\n }\n\n const data = await response.json();\n\n onSuccess?.(userId);\n\n // Redirect to returnUrl\n window.location.href = data.returnUrl || returnUrl;\n }\n catch (err)\n {\n const message = err instanceof Error ? err.message : 'OAuth failed';\n setError(message);\n setIsLoading(false);\n onError?.(message);\n }\n }\n\n finalizeOAuth();\n }, [apiBasePath, onSuccess, onError]);\n\n if (error)\n {\n if (errorComponent)\n {\n return <>{errorComponent(error)}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <h2>Authentication Error</h2>\n <p style={{ color: 'red' }}>{error}</p>\n <button onClick={() => window.location.href = '/'}>\n Go Home\n </button>\n </div>\n );\n }\n\n if (isLoading)\n {\n if (loadingComponent)\n {\n return <>{loadingComponent}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <p>Completing authentication...</p>\n </div>\n );\n }\n\n return null;\n}\n\nexport default OAuthCallback;\n"],"mappings":";;;AAeA,SAAS,WAAW,gBAAgB;AA8GjB,wBAIP,YAJO;AA/EZ,SAAS,cAAc;AAAA,EAC1B,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,GACA;AACI,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MACV;AACI,mBAAe,gBACf;AACI,UACA;AACI,cAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,cAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,cAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,cAAM,YAAY,OAAO,IAAI,WAAW,KAAK;AAC7C,cAAM,aAAa,OAAO,IAAI,OAAO;AAGrC,YAAI,YACJ;AACI,gBAAM,IAAI,MAAM,UAAU;AAAA,QAC9B;AAEA,YAAI,CAAC,UAAU,CAAC,OAChB;AACI,gBAAM,IAAI,MAAM,6BAA6B;AAAA,QACjD;AAGA,cAAM,WAAW,MAAM,MAAM,GAAG,WAAW,kBAAkB;AAAA,UACzD,QAAQ;AAAA,UACR,SAAS;AAAA,YACL,gBAAgB;AAAA,UACpB;AAAA,UACA,aAAa;AAAA,UACb,MAAM,KAAK,UAAU;AAAA,YACjB,MAAM;AAAA,cACF;AAAA,cACA;AAAA,cACA;AAAA,YACJ;AAAA,UACJ,CAAC;AAAA,QACL,CAAC;AAED,YAAI,CAAC,SAAS,IACd;AACI,gBAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,gBAAM,IAAI,MAAMA,MAAK,WAAW,0BAA0B;AAAA,QAC9D;AAEA,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,oBAAY,MAAM;AAGlB,eAAO,SAAS,OAAO,KAAK,aAAa;AAAA,MAC7C,SACO,KACP;AACI,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,qBAAa,KAAK;AAClB,kBAAU,OAAO;AAAA,MACrB;AAAA,IACJ;AAEA,kBAAc;AAAA,EAClB,GAAG,CAAC,aAAa,WAAW,OAAO,CAAC;AAEpC,MAAI,OACJ;AACI,QAAI,gBACJ;AACI,aAAO,gCAAG,yBAAe,KAAK,GAAE;AAAA,IACpC;AAEA,WACI,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C;AAAA,0BAAC,QAAG,kCAAoB;AAAA,MACxB,oBAAC,OAAE,OAAO,EAAE,OAAO,MAAM,GAAI,iBAAM;AAAA,MACnC,oBAAC,YAAO,SAAS,MAAM,OAAO,SAAS,OAAO,KAAK,qBAEnD;AAAA,OACJ;AAAA,EAER;AAEA,MAAI,WACJ;AACI,QAAI,kBACJ;AACI,aAAO,gCAAG,4BAAiB;AAAA,IAC/B;AAEA,WACI,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C,8BAAC,OAAE,0CAA4B,GACnC;AAAA,EAER;AAEA,SAAO;AACX;","names":["data"]}
|
package/dist/nextjs/server.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { SessionData } from '
|
|
4
|
-
|
|
3
|
+
import { S as SessionData } from '../session-Dbvz9Sdp.js';
|
|
4
|
+
import { K as KeyAlgorithmType } from '../types-B1CzVZkU.js';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
5
6
|
|
|
6
7
|
interface RequireAuthProps {
|
|
7
8
|
/**
|
|
@@ -138,6 +139,29 @@ declare function RequirePermission({ permissions, children, redirectTo, fallback
|
|
|
138
139
|
*
|
|
139
140
|
* Uses authApi to check permissions in real-time
|
|
140
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* Get current auth session with roles and permissions via API
|
|
144
|
+
*/
|
|
145
|
+
declare function getAuthSessionData(): Promise<{
|
|
146
|
+
role: {
|
|
147
|
+
id: number;
|
|
148
|
+
name: string;
|
|
149
|
+
displayName: string;
|
|
150
|
+
priority: number;
|
|
151
|
+
};
|
|
152
|
+
permissions: {
|
|
153
|
+
id: number;
|
|
154
|
+
name: string;
|
|
155
|
+
displayName: string;
|
|
156
|
+
category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
|
|
157
|
+
}[];
|
|
158
|
+
userId: number;
|
|
159
|
+
publicId: string;
|
|
160
|
+
email: string | null;
|
|
161
|
+
emailVerified: boolean;
|
|
162
|
+
phoneVerified: boolean;
|
|
163
|
+
hasPassword: boolean;
|
|
164
|
+
} | null>;
|
|
141
165
|
/**
|
|
142
166
|
* Get user role
|
|
143
167
|
*/
|
|
@@ -161,6 +185,14 @@ declare function hasAnyPermission(requiredPermissions: string[]): Promise<boolea
|
|
|
161
185
|
* Server-side only (uses next/headers)
|
|
162
186
|
*/
|
|
163
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Pending OAuth session data (before user ID is known)
|
|
190
|
+
*/
|
|
191
|
+
interface PendingSessionData {
|
|
192
|
+
privateKey: string;
|
|
193
|
+
keyId: string;
|
|
194
|
+
algorithm: KeyAlgorithmType;
|
|
195
|
+
}
|
|
164
196
|
/**
|
|
165
197
|
* Public session information (excludes sensitive data)
|
|
166
198
|
*/
|
|
@@ -224,5 +256,62 @@ declare function getSession(): Promise<PublicSession | null>;
|
|
|
224
256
|
* Clear session cookie
|
|
225
257
|
*/
|
|
226
258
|
declare function clearSession(): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Seal pending session data (for OAuth flow)
|
|
261
|
+
*
|
|
262
|
+
* @param data - Pending session data (privateKey, keyId, algorithm)
|
|
263
|
+
* @param ttl - Time to live in seconds (default: 10 minutes)
|
|
264
|
+
*/
|
|
265
|
+
declare function sealPendingSession(data: PendingSessionData, ttl?: number): Promise<string>;
|
|
266
|
+
/**
|
|
267
|
+
* Unseal pending session data
|
|
268
|
+
*
|
|
269
|
+
* @param jwt - Encrypted pending session token
|
|
270
|
+
*/
|
|
271
|
+
declare function unsealPendingSession(jwt: string): Promise<PendingSessionData>;
|
|
272
|
+
/**
|
|
273
|
+
* Get pending session from cookie
|
|
274
|
+
*/
|
|
275
|
+
declare function getPendingSession(): Promise<PendingSessionData | null>;
|
|
276
|
+
/**
|
|
277
|
+
* Clear pending session cookie
|
|
278
|
+
*/
|
|
279
|
+
declare function clearPendingSession(): Promise<void>;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* OAuth Handlers for Next.js
|
|
283
|
+
*
|
|
284
|
+
* Helper functions to create OAuth callback route handlers
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
interface OAuthCallbackOptions {
|
|
288
|
+
/**
|
|
289
|
+
* Default redirect URL if returnUrl is not provided
|
|
290
|
+
* @default '/'
|
|
291
|
+
*/
|
|
292
|
+
defaultRedirectUrl?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Error redirect URL
|
|
295
|
+
* @default '/auth/error'
|
|
296
|
+
*/
|
|
297
|
+
errorRedirectUrl?: string;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Create OAuth callback handler for Next.js API Route
|
|
301
|
+
*
|
|
302
|
+
* Handles the final step of OAuth flow:
|
|
303
|
+
* 1. Gets userId, keyId from query params (set by backend)
|
|
304
|
+
* 2. Gets privateKey from pending session cookie
|
|
305
|
+
* 3. Creates full session and saves to cookie
|
|
306
|
+
* 4. Redirects to returnUrl
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* // /api/auth/callback/route.ts
|
|
311
|
+
* import { createOAuthCallbackHandler } from '@spfn/auth/nextjs/server';
|
|
312
|
+
* export const GET = createOAuthCallbackHandler();
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
declare function createOAuthCallbackHandler(options?: OAuthCallbackOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
227
316
|
|
|
228
|
-
export { type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, clearSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession };
|
|
317
|
+
export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, SessionData, clearPendingSession, clearSession, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, unsealPendingSession };
|