fix(account-delete): judge step-up auth by amr sign-in time, not token iat

This commit is contained in:
Yun Chan 2026-09-28 00:54:00 +09:00
parent 043ef579a8
commit 83c2deb561
3 changed files with 167 additions and 19 deletions

View file

@ -1,13 +1,13 @@
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts'
import { createServiceRoleClient } from '../_shared/quota.ts'
import { hasRecentAuthentication } from './recent-auth.ts'
interface DeleteAccountRequest {
confirmation: string
}
const CONFIRMATION_PHRASE = 'DELETE_MY_ACCOUNT'
const RECENT_AUTH_SECONDS = 10 * 60
const STORAGE_BUCKETS = ['audio', 'exports', 'avatars'] as const
function jsonResponse(body: Record<string, unknown>, status = 200): Response {
@ -17,22 +17,6 @@ function jsonResponse(body: Record<string, unknown>, status = 200): Response {
})
}
function decodeJwtIssuedAt(authorization: string | null): number | null {
const token = authorization?.replace(/^Bearer\s+/i, '')
if (!token) return null
const payloadPart = token.split('.')[1]
if (!payloadPart) return null
try {
const normalized = payloadPart.replace(/-/g, '+').replace(/_/g, '/')
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
const payload = JSON.parse(atob(padded)) as { iat?: unknown }
return typeof payload.iat === 'number' ? payload.iat : null
} catch {
return null
}
}
async function listStorageFiles(
serviceClient: ReturnType<typeof createServiceRoleClient>,
bucket: string,
@ -81,9 +65,10 @@ Deno.serve(async (req: Request) => {
return jsonResponse({ error: 'Explicit confirmation is required', code: 'CONFIRMATION_REQUIRED' }, 400)
}
const issuedAt = decodeJwtIssuedAt(req.headers.get('Authorization'))
// Step-up auth: judged by the session's amr authentication time, never the
// access token iat (which every refresh_token grant resets).
const nowSeconds = Math.floor(Date.now() / 1000)
if (issuedAt === null || nowSeconds - issuedAt > RECENT_AUTH_SECONDS || issuedAt > nowSeconds + 60) {
if (!hasRecentAuthentication(req.headers.get('Authorization'), nowSeconds)) {
return jsonResponse({ error: 'Recent authentication is required', code: 'REAUTHENTICATION_REQUIRED' }, 403)
}