requests.js (6089B)
1 import crypto from 'node:crypto'; 2 import { parseCookie, stringifySetCookie } from 'cookie'; 3 4 const AUTH_REQUEST_TTL = 10 * 60; 5 const CODE_TTL = 60; 6 7 export function authRequestCookieName(realm) { 8 return `auth_request_${realm.slug}`; 9 } 10 11 function secureRequest(req) { 12 if (req.socket?.encrypted) return true; 13 const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase(); 14 return proto === 'https'; 15 } 16 17 function appendSetCookie(res, value) { 18 const existing = res.getHeader('Set-Cookie'); 19 if (!existing) { 20 res.setHeader('Set-Cookie', value); 21 return; 22 } 23 const values = Array.isArray(existing) ? existing : [existing]; 24 res.setHeader('Set-Cookie', [...values, value]); 25 } 26 27 export function setAuthRequestCookie(res, req, realm, id, ttl = AUTH_REQUEST_TTL) { 28 appendSetCookie( 29 res, 30 stringifySetCookie({ 31 name: authRequestCookieName(realm), 32 value: id, 33 httpOnly: true, 34 sameSite: 'lax', 35 path: '/', 36 maxAge: ttl, 37 secure: secureRequest(req), 38 }) 39 ); 40 } 41 42 export function clearAuthRequestCookie(res, req, realm) { 43 appendSetCookie( 44 res, 45 stringifySetCookie({ 46 name: authRequestCookieName(realm), 47 value: '', 48 httpOnly: true, 49 sameSite: 'lax', 50 path: '/', 51 maxAge: 0, 52 secure: secureRequest(req), 53 }) 54 ); 55 } 56 57 export function readAuthRequestId(req, realm) { 58 const cookies = parseCookie(req.headers.cookie || ''); 59 return cookies[authRequestCookieName(realm)] || null; 60 } 61 62 export async function purgeExpired(db, now = Date.now()) { 63 await db.run('DELETE FROM auth_requests WHERE expires_at <= {now}', { now }); 64 await db.run('DELETE FROM authorization_codes WHERE expires_at <= {now}', { now }); 65 } 66 67 export async function createAuthRequest(db, data, now = Date.now()) { 68 const id = crypto.randomBytes(24).toString('base64url'); 69 await db.run( 70 `INSERT INTO auth_requests 71 (id, client_id, redirect_uri, scope, state, nonce, code_challenge, 72 code_challenge_method, created_at, expires_at) 73 VALUES 74 ({id}, {clientId}, {redirectUri}, {scope}, {state}, {nonce}, {codeChallenge}, 75 {codeChallengeMethod}, {createdAt}, {expiresAt})`, 76 { 77 id, 78 clientId: data.clientId, 79 redirectUri: data.redirectUri, 80 scope: data.scope, 81 state: data.state || null, 82 nonce: data.nonce || null, 83 codeChallenge: data.codeChallenge || null, 84 codeChallengeMethod: data.codeChallengeMethod || null, 85 createdAt: now, 86 expiresAt: now + AUTH_REQUEST_TTL * 1000, 87 } 88 ); 89 return id; 90 } 91 92 export async function getAuthRequest(db, id, now = Date.now()) { 93 if (!id) return null; 94 const request = await db.get( 95 `SELECT id, client_id, redirect_uri, scope, state, nonce, code_challenge, 96 code_challenge_method, session_id, user_id, auth_time, 97 created_at, expires_at, completed_at 98 FROM auth_requests 99 WHERE id = {id}`, 100 { id } 101 ); 102 if (!request) return null; 103 if (request.expires_at <= now) { 104 await db.run('DELETE FROM auth_requests WHERE id = {id}', { id }); 105 return null; 106 } 107 return request; 108 } 109 110 export async function selectAuthRequestAccount( 111 db, 112 id, 113 { userId, sessionId, authTime } 114 ) { 115 if (!id) return; 116 await db.run( 117 `UPDATE auth_requests 118 SET user_id = {userId}, session_id = {sessionId}, auth_time = {authTime} 119 WHERE id = {id} AND completed_at IS NULL`, 120 { id, userId, sessionId, authTime } 121 ); 122 } 123 124 export async function clearAuthRequestSelection(db, id) { 125 if (!id) return; 126 await db.run( 127 `UPDATE auth_requests 128 SET user_id = NULL, session_id = NULL, auth_time = NULL 129 WHERE id = {id} AND completed_at IS NULL`, 130 { id } 131 ); 132 } 133 134 export async function completeAuthRequest(db, id, { userId, sessionId, authTime }, now = Date.now()) { 135 await db.run( 136 `UPDATE auth_requests 137 SET user_id = {userId}, session_id = {sessionId}, auth_time = {authTime}, completed_at = {now} 138 WHERE id = {id}`, 139 { id, userId, sessionId, authTime, now } 140 ); 141 } 142 143 export async function deleteAuthRequest(db, id) { 144 if (!id) return; 145 await db.run('DELETE FROM auth_requests WHERE id = {id}', { id }); 146 } 147 148 export async function createAuthorizationCode(db, request, now = Date.now()) { 149 const code = crypto.randomBytes(32).toString('base64url'); 150 const id = crypto.createHash('sha256').update(code).digest('hex'); 151 152 await db.run( 153 `INSERT INTO authorization_codes 154 (id, auth_request_id, client_id, user_id, redirect_uri, scope, nonce, 155 code_challenge, code_challenge_method, session_id, auth_time, 156 created_at, expires_at) 157 VALUES 158 ({id}, {authRequestId}, {clientId}, {userId}, {redirectUri}, {scope}, {nonce}, 159 {codeChallenge}, {codeChallengeMethod}, {sessionId}, {authTime}, 160 {createdAt}, {expiresAt})`, 161 { 162 id, 163 authRequestId: request.id, 164 clientId: request.client_id, 165 userId: request.user_id, 166 redirectUri: request.redirect_uri, 167 scope: request.scope, 168 nonce: request.nonce, 169 codeChallenge: request.code_challenge, 170 codeChallengeMethod: request.code_challenge_method, 171 sessionId: request.session_id, 172 authTime: request.auth_time, 173 createdAt: now, 174 expiresAt: now + CODE_TTL * 1000, 175 } 176 ); 177 178 return code; 179 } 180 181 export async function consumeAuthorizationCode(db, code, clientId, now = Date.now()) { 182 if (!code) return null; 183 const id = crypto.createHash('sha256').update(String(code)).digest('hex'); 184 185 return db.transaction(async (tx) => { 186 const row = await tx.get( 187 `SELECT id, auth_request_id, client_id, user_id, redirect_uri, scope, nonce, 188 code_challenge, code_challenge_method, session_id, auth_time, 189 expires_at, consumed_at 190 FROM authorization_codes 191 WHERE id = {id}`, 192 { id } 193 ); 194 if (!row || row.consumed_at || row.expires_at <= now || row.client_id !== clientId) { 195 return null; 196 } 197 await tx.run('UPDATE authorization_codes SET consumed_at = {now} WHERE id = {id}', { now, id }); 198 return row; 199 }); 200 }