//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Api.Controllers.Security; using System.Text.Encodings.Web; using AliasServerDb; using AliasVault.Api.Controllers.Abstracts; using AliasVault.Auth; using AliasVault.Shared.Models.Enums; using Asp.Versioning; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; /// /// Two-factor authentication controller for handling two-factor authentication related actions. /// /// AliasServerDbContext instance. /// UrlEncoder instance. /// AuthLoggingService instance. This is used to log auth attempts to the database. /// UserManager instance. [Route("v{version:apiVersion}/[controller]")] [ApiController] [ApiVersion("1")] public class TwoFactorAuthController(IDbContextFactory dbContextFactory, UrlEncoder urlEncoder, AuthLoggingService authLoggingService, UserManager userManager) : AuthenticatedRequestController(userManager) { /// /// Get two-factor authentication enabled status for a user. /// /// Task. [HttpGet("status")] public async Task Status() { var user = await GetCurrentUserAsync(); if (user is null) { return Unauthorized(); } var twoFactorEnabled = await GetUserManager().GetTwoFactorEnabledAsync(user); return Ok(new { TwoFactorEnabled = twoFactorEnabled }); } /// /// Enable two-factor authentication for a user. /// /// Task. [HttpPost("enable")] public async Task Enable() { var user = await GetCurrentUserAsync(); if (user is null) { return Unauthorized(); } string? authenticatorKey; authenticatorKey = await GetUserManager().GetAuthenticatorKeyAsync(user); // Only reset (create new keys) if no key exists yet, avoiding duplicate key errors. if (string.IsNullOrEmpty(authenticatorKey)) { try { await GetUserManager().ResetAuthenticatorKeyAsync(user); authenticatorKey = await GetUserManager().GetAuthenticatorKeyAsync(user); } catch (DbUpdateException) { // Key was most likely created by concurrent request, just get it. authenticatorKey = await GetUserManager().GetAuthenticatorKeyAsync(user); } } var encodedKey = urlEncoder.Encode(authenticatorKey!); var qrCodeUrl = $"otpauth://totp/{urlEncoder.Encode("AliasVault")}:{urlEncoder.Encode(user.UserName!)}?secret={encodedKey}&issuer={urlEncoder.Encode("AliasVault")}"; return Ok(new { Secret = authenticatorKey, QrCodeUrl = qrCodeUrl }); } /// /// Verify two-factor authentication setup. /// /// Code to verify if 2fa successfully works. /// Task. [HttpPost("verify")] public async Task Verify([FromBody] string code) { var user = await GetCurrentUserAsync(); if (user is null) { return Unauthorized(); } var isValid = await GetUserManager().VerifyTwoFactorTokenAsync(user, GetUserManager().Options.Tokens.AuthenticatorTokenProvider, code); if (isValid) { try { await GetUserManager().SetTwoFactorEnabledAsync(user, true); // Generate new recovery codes. var recoveryCodes = await GetUserManager().GenerateNewTwoFactorRecoveryCodesAsync(user, 10); await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TwoFactorAuthEnable); return Ok(new { RecoveryCodes = recoveryCodes }); } catch (DbUpdateException) { // Likely a concurrent request already enabled 2FA, still return success. var recoveryCodes = await GetUserManager().GenerateNewTwoFactorRecoveryCodesAsync(user, 10); return Ok(new { RecoveryCodes = recoveryCodes }); } } return BadRequest("Invalid code."); } /// /// Disable two-factor authentication for a user. /// /// Task. [HttpPost("disable")] public async Task Disable() { var user = await GetCurrentUserAsync(); if (user is null) { return Unauthorized(); } await using var context = await dbContextFactory.CreateDbContextAsync(); await using var transaction = await context.Database.BeginTransactionAsync(); try { // Disable 2FA and remove any existing authenticator key(s) and recovery codes. await GetUserManager().SetTwoFactorEnabledAsync(user, false); context.UserTokens.RemoveRange( await context.UserTokens.Where( x => x.UserId == user.Id && (x.Name == "AuthenticatorKey" || x.Name == "RecoveryCodes")).ToListAsync()); await context.SaveChangesAsync(); await transaction.CommitAsync(); await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TwoFactorAuthDisable); return Ok(); } catch { await transaction.RollbackAsync(); throw; } } }