Improve security by requiring higher authentication levels for sensitive actions

Update authentication flows to enforce AAL2 requirements for MFA operations and identity disconnections, and adjust TOTP verification logic.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: da324197-4d44-4e4b-b342-fe8ae33cf0cf
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-10-27 23:53:33 +00:00
parent 64f82c9ac2
commit d1f01d9228
5 changed files with 62 additions and 9 deletions

View File

@@ -38,10 +38,6 @@ externalPort = 80
localPort = 5001 localPort = 5001
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 34475
externalPort = 3003
[[ports]] [[ports]]
localPort = 37143 localPort = 37143
externalPort = 3001 externalPort = 3001

View File

@@ -225,9 +225,16 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
setLoading(true); setLoading(true);
try { try {
// Verify TOTP code // Get the factor ID first
const factorId = (await supabase.auth.mfa.listFactors()).data?.totp?.[0]?.id || '';
if (!factorId) {
throw new Error('No MFA factor found');
}
// Create challenge
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
factorId: (await supabase.auth.mfa.listFactors()).data?.totp?.[0]?.id || '' factorId
}); });
if (challengeError) { if (challengeError) {
@@ -240,8 +247,9 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
throw challengeError; throw challengeError;
} }
// Verify TOTP code with correct factorId
const { error: verifyError } = await supabase.auth.mfa.verify({ const { error: verifyError } = await supabase.auth.mfa.verify({
factorId: challengeData.id, factorId,
challengeId: challengeData.id, challengeId: challengeData.id,
code: totpCode code: totpCode
}); });

View File

@@ -90,12 +90,59 @@ export async function checkDisconnectSafety(
/** /**
* Disconnect an OAuth identity from the user's account * Disconnect an OAuth identity from the user's account
* Requires AAL2 session for security
*/ */
export async function disconnectIdentity( export async function disconnectIdentity(
provider: OAuthProvider provider: OAuthProvider
): Promise<IdentityOperationResult> { ): Promise<IdentityOperationResult> {
try { try {
// Safety check first // AAL2 check for security-critical operation (MUST fail closed)
const { data: { session } } = await supabase.auth.getSession();
// Get AAL level - fail closed on error
const { data: aalData, error: aalError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
if (aalError) {
logger.error('Failed to get AAL level for identity disconnect', {
action: 'disconnect_identity_aal_check',
error: aalError.message
});
return {
success: false,
error: 'Unable to verify security level. Please try again.',
requiresAAL2: true
};
}
const currentAal = aalData?.currentLevel || 'aal1';
// If not at AAL2, check if MFA is enrolled - fail closed on error
if (currentAal !== 'aal2') {
const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors();
if (factorsError) {
logger.error('Failed to list MFA factors for identity disconnect', {
action: 'disconnect_identity_mfa_check',
error: factorsError.message
});
return {
success: false,
error: 'Unable to verify MFA status. Please try again.',
requiresAAL2: true
};
}
const hasEnrolledMFA = factors?.totp?.some(f => f.status === 'verified') || false;
if (hasEnrolledMFA) {
return {
success: false,
error: 'Please verify your identity with MFA before disconnecting accounts',
requiresAAL2: true
};
}
}
// Safety check
const safetyCheck = await checkDisconnectSafety(provider); const safetyCheck = await checkDisconnectSafety(provider);
if (!safetyCheck.canDisconnect) { if (!safetyCheck.canDisconnect) {
return { return {

View File

@@ -33,4 +33,5 @@ export interface IdentityOperationResult {
needsRelogin?: boolean; needsRelogin?: boolean;
needsEmailConfirmation?: boolean; needsEmailConfirmation?: boolean;
email?: string; email?: string;
requiresAAL2?: boolean;
} }

View File

@@ -60,7 +60,8 @@ Deno.serve(async (req) => {
// Phase 1: Check AAL level // Phase 1: Check AAL level
const { data: { session } } = await supabaseClient.auth.getSession(); const { data: { session } } = await supabaseClient.auth.getSession();
const aal = session?.aal || 'aal1'; const { data: aalData } = await supabaseClient.auth.mfa.getAuthenticatorAssuranceLevel();
const aal = aalData?.currentLevel || 'aal1';
if (aal !== 'aal2') { if (aal !== 'aal2') {
edgeLogger.warn('AAL2 required for MFA removal', { action: 'mfa_unenroll_aal', userId: user.id, aal }); edgeLogger.warn('AAL2 required for MFA removal', { action: 'mfa_unenroll_aal', userId: user.id, aal });