Add new user sessions endpoint to webapi (#80)

This commit is contained in:
Leendert de Borst
2024-08-30 13:54:31 +02:00
parent 188b1cba94
commit 4f8ab5da28
11 changed files with 160 additions and 82 deletions

View File

@@ -5,7 +5,7 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
namespace AliasVault.Api.Controllers.Abstracts;
using System.Security.Claims;
using AliasServerDb;
@@ -20,8 +20,14 @@ using Microsoft.AspNetCore.Mvc;
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[Authorize]
public class AuthenticatedRequestController(UserManager<AliasVaultUser> userManager) : ControllerBase
public abstract class AuthenticatedRequestController(UserManager<AliasVaultUser> userManager) : ControllerBase
{
/// <summary>
/// Get the userManager instance.
/// </summary>
/// <returns>UserManager instance.</returns>
protected UserManager<AliasVaultUser> GetUserManager() => userManager;
/// <summary>
/// Get the current authenticated user.
/// </summary>

View File

@@ -5,9 +5,10 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
namespace AliasVault.Api.Controllers.Email;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Api.Helpers;
using AliasVault.Shared.Models.Spamok;
using AliasVault.Shared.Models.WebApi;

View File

@@ -5,9 +5,10 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
namespace AliasVault.Api.Controllers.Email;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Api.Helpers;
using AliasVault.Shared.Models.Spamok;
using Asp.Versioning;

View File

@@ -8,6 +8,7 @@
namespace AliasVault.Api.Controllers;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Shared.Models.WebApi.Favicon;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;

View File

@@ -1,39 +0,0 @@
//-----------------------------------------------------------------------
// <copyright file="IdentityController.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
using AliasGenerators.Identity.Implementations;
using AliasServerDb;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Controller for identity generation.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class IdentityController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Proxies the request to the english identity generator to generate a random identity.
/// </summary>
/// <returns>Identity model.</returns>
[HttpGet("Generate")]
public async Task<IActionResult> Generate()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return Unauthorized();
}
var identityGenerator = new IdentityGeneratorEn();
return Ok(await identityGenerator.GenerateRandomIdentityAsync());
}
}

View File

@@ -0,0 +1,86 @@
//-----------------------------------------------------------------------
// <copyright file="SecurityController.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers.Security;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.AuthLogging;
using AliasVault.Shared.Models.WebApi.Security;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// Security controller for handling security related actions such as showing auth logs or revoking sessions for user.
/// </summary>
/// <param name="dbContextFactory">AliasServerDbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class SecurityController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Returns list of active sessions (refresh tokens) for the current user.
/// </summary>
/// <returns>Task with list of active refresh tokens.</returns>
[HttpGet("sessions")]
public async Task<IActionResult> Sessions()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return Unauthorized("Not authenticated.");
}
await using var context = await dbContextFactory.CreateDbContextAsync();
var refreshTokenList = await context.AliasVaultUserRefreshTokens.Where(x => x.UserId == user.Id).Select(x => new RefreshTokenModel()
{
Id = x.Id,
DeviceIdentifier = x.DeviceIdentifier,
ExpireDate = x.ExpireDate,
CreatedAt = x.CreatedAt,
})
.OrderBy(x => x.CreatedAt)
.ToListAsync();
return Ok(refreshTokenList);
}
/// <summary>
/// Revokes a specific user session (refresh token).
/// </summary>
/// <param name="id">The ID of the refresh token to revoke.</param>
/// <returns>Http200 if success.</returns>
[HttpDelete("sessions/{id}")]
public async Task<IActionResult> RevokeSession(Guid id)
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return Unauthorized("Not authenticated.");
}
await using var context = await dbContextFactory.CreateDbContextAsync();
var refreshToken = await context.AliasVaultUserRefreshTokens
.FirstOrDefaultAsync(x => x.Id == id && x.UserId == user.Id);
if (refreshToken == null)
{
return NotFound("Session not found or does not belong to the current user.");
}
context.AliasVaultUserRefreshTokens.Remove(refreshToken);
await context.SaveChangesAsync();
return Ok();
}
}

View File

@@ -5,10 +5,11 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
namespace AliasVault.Api.Controllers.Security;
using System.Text.Encodings.Web;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.AuthLogging;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
@@ -19,13 +20,13 @@ using Microsoft.EntityFrameworkCore;
/// Auth controller for handling authentication.
/// </summary>
/// <param name="dbContextFactory">AliasServerDbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
/// <param name="urlEncoder">UrlEncoder instance.</param>
/// <param name="authLoggingService">AuthLoggingService instance. This is used to log auth attempts to the database.</param>
/// <param name="userManager">UserManager instance.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, UrlEncoder urlEncoder, AuthLoggingService authLoggingService) : ControllerBase
public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UrlEncoder urlEncoder, AuthLoggingService authLoggingService, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Get two-factor authentication enabled status for a user.
@@ -34,13 +35,13 @@ public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbC
[HttpGet("status")]
public async Task<IActionResult> Status()
{
var user = await userManager.GetUserAsync(User);
if (user == null)
var user = await GetCurrentUserAsync();
if (user is null)
{
return NotFound();
return Unauthorized("Not authenticated.");
}
var twoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user);
var twoFactorEnabled = await GetUserManager().GetTwoFactorEnabledAsync(user);
return Ok(new { TwoFactorEnabled = twoFactorEnabled });
}
@@ -51,17 +52,17 @@ public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbC
[HttpPost("enable")]
public async Task<IActionResult> Enable()
{
var user = await userManager.GetUserAsync(User);
if (user == null)
var user = await GetCurrentUserAsync();
if (user is null)
{
return NotFound();
return Unauthorized("Not authenticated.");
}
var authenticatorKey = await userManager.GetAuthenticatorKeyAsync(user);
var authenticatorKey = await GetUserManager().GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(authenticatorKey))
{
await userManager.ResetAuthenticatorKeyAsync(user);
authenticatorKey = await userManager.GetAuthenticatorKeyAsync(user);
await GetUserManager().ResetAuthenticatorKeyAsync(user);
authenticatorKey = await GetUserManager().GetAuthenticatorKeyAsync(user);
}
var encodedKey = urlEncoder.Encode(authenticatorKey!);
@@ -78,20 +79,20 @@ public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbC
[HttpPost("verify")]
public async Task<IActionResult> Verify([FromBody] string code)
{
var user = await userManager.GetUserAsync(User);
if (user == null)
var user = await GetCurrentUserAsync();
if (user is null)
{
return NotFound();
return Unauthorized("Not authenticated.");
}
var isValid = await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, code);
var isValid = await GetUserManager().VerifyTwoFactorTokenAsync(user, GetUserManager().Options.Tokens.AuthenticatorTokenProvider, code);
if (isValid)
{
await userManager.SetTwoFactorEnabledAsync(user, true);
await GetUserManager().SetTwoFactorEnabledAsync(user, true);
// Generate new recovery codes.
var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var recoveryCodes = await GetUserManager().GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TwoFactorAuthEnable);
@@ -108,16 +109,16 @@ public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbC
[HttpPost("disable")]
public async Task<IActionResult> Disable()
{
var user = await userManager.GetUserAsync(User);
if (user == null)
var user = await GetCurrentUserAsync();
if (user is null)
{
return NotFound();
return Unauthorized("Not authenticated.");
}
await using var context = await dbContextFactory.CreateDbContextAsync();
// Disable 2FA and remove any existing authenticator key(s) and recovery codes.
await userManager.SetTwoFactorEnabledAsync(user, false);
await GetUserManager().SetTwoFactorEnabledAsync(user, false);
context.UserTokens.RemoveRange(
await context.UserTokens.Where(
x => x.UserId == user.Id &&

View File

@@ -11,9 +11,10 @@
* attack surfaces we don't include this file in the production build.
*/
namespace AliasVault.Api.Controllers;
namespace AliasVault.Api.Controllers.Tests;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;

View File

@@ -9,6 +9,7 @@ namespace AliasVault.Api.Controllers;
using System.ComponentModel.DataAnnotations;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Api.Helpers;
using AliasVault.Api.Vault;
using AliasVault.Api.Vault.RetentionRules;

View File

@@ -97,21 +97,6 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
return domainToUse;
}
/// <summary>
/// Generate random identity by calling the IdentityGenerator API.
/// </summary>
/// <returns>Identity object.</returns>
public async Task<Identity> GenerateRandomIdentityAsync()
{
var identity = await httpClient.GetFromJsonAsync<Identity>("api/v1/Identity/Generate");
if (identity is null)
{
throw new InvalidOperationException("Failed to generate random identity.");
}
return identity;
}
/// <summary>
/// Insert new entry into database.
/// </summary>

View File

@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="RefreshTokenModel.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Shared.Models.WebApi.Security;
/// <summary>
/// RefreshToken (user session) model.
/// </summary>
public class RefreshTokenModel
{
/// <summary>
/// Gets or sets the unique identifier for the refresh token.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the device identifier associated with the refresh token.
/// </summary>
public string DeviceIdentifier { get; set; } = null!;
/// <summary>
/// Gets or sets the expiration date of the refresh token.
/// </summary>
public DateTime ExpireDate { get; set; }
/// <summary>
/// Gets or sets the creation date of the refresh token.
/// </summary>
public DateTime CreatedAt { get; set; }
}