mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
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:
4
.replit
4
.replit
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -33,4 +33,5 @@ export interface IdentityOperationResult {
|
|||||||
needsRelogin?: boolean;
|
needsRelogin?: boolean;
|
||||||
needsEmailConfirmation?: boolean;
|
needsEmailConfirmation?: boolean;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
requiresAAL2?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user