diff --git a/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpAuthorizationHandler.cs b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpAuthorizationHandler.cs new file mode 100644 index 0000000..523072b --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpAuthorizationHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Passwordless.AspNetIdentity.Example.Authorization; + +public interface IStepUpAuthorizationRequirement : IAuthorizationRequirement +{ + public string Name { get; } +} + +public class StepUpAuthorizationHandler(StepUpPurpose stepUpPurpose, TimeProvider timeProvider) : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IStepUpAuthorizationRequirement requirement) + { + if (context.User.Identity is not { IsAuthenticated: true }) + { + return Task.CompletedTask; + } + + if (context.User.HasClaim(MatchesClaim(requirement)) + && IsExpired(GetExpiration(context.User.FindFirst(MatchesClaim(requirement))!))) + { + context.Succeed(requirement); + } + else + { + stepUpPurpose.Purpose = requirement.Name; + context.Fail(); + } + + return Task.CompletedTask; + } + + private static Predicate MatchesClaim(IStepUpAuthorizationRequirement requirement) => claim => claim.Type == requirement.Name; + + private bool IsExpired(DateTime expiration) + { + return expiration > timeProvider.GetUtcNow().DateTime; + } + + private static DateTime GetExpiration(Claim claim) + { + var expiration = DateTime.Parse(claim.Value, null, DateTimeStyles.RoundtripKind); + + return expiration; + } +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurpose.cs b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurpose.cs new file mode 100644 index 0000000..83a43b1 --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurpose.cs @@ -0,0 +1,6 @@ +namespace Passwordless.AspNetIdentity.Example.Authorization; + +public class StepUpPurpose +{ + public string Purpose { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurposes.cs b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurposes.cs new file mode 100644 index 0000000..7574d08 --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpPurposes.cs @@ -0,0 +1,6 @@ +namespace Passwordless.AspNetIdentity.Example.Authorization; + +public static class StepUpPurposes +{ + public const string StepUp = "step-up"; +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpRequirement.cs b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpRequirement.cs new file mode 100644 index 0000000..018816a --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Authorization/StepUpRequirement.cs @@ -0,0 +1,6 @@ +namespace Passwordless.AspNetIdentity.Example.Authorization; + +public class StepUpRequirement(string policyName) : IStepUpAuthorizationRequirement +{ + public string Name => policyName; +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml index c8519e3..06bf5b1 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml @@ -26,18 +26,19 @@ - + If you have lost your passkey, please click here. @if (canLogin) { - - } \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Logout.cshtml.cs b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Logout.cshtml.cs index 2daef9a..a543ee4 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Logout.cshtml.cs +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Logout.cshtml.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -10,6 +12,11 @@ public class Logout(SignInManager userSignInManager, ILogger OnGet() { + var userManager = userSignInManager.UserManager; + + var user = await userManager.GetUserAsync(User); + if (user is not null) await userManager.RemoveClaimsAsync(user, await userManager.GetClaimsAsync(user)); + await userSignInManager.SignOutAsync(); logger.LogInformation("User has signed out."); diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Magic.cshtml.cs b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Magic.cshtml.cs index 0aefb7b..b0127ee 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Magic.cshtml.cs +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Magic.cshtml.cs @@ -1,4 +1,7 @@ -using System.Threading; +using System; +using System.Globalization; +using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -20,6 +23,8 @@ public Magic(PasswordlessClient passwordlessClient, public bool Success { get; set; } + private static readonly string[] GeneratedTokenTypes = ["magic_link", "generated_signin"]; + public async Task OnGet(string token) { if (User.Identity is { IsAuthenticated: true }) return RedirectToPage("/Authorized/HelloWorld"); @@ -47,6 +52,13 @@ public async Task OnGet(string token) } await _signInManager.SignInAsync(user, true); + + + if (GeneratedTokenTypes.Contains(response.Type)) + { + await _signInManager.UserManager.AddClaimAsync(user, new Claim(response.Type, DateTime.UtcNow.ToString(CultureInfo.InvariantCulture))); + } + Success = true; return LocalRedirect("/Authorized/HelloWorld"); } diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml index 764b0d0..617a983 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml @@ -2,6 +2,12 @@ @using Microsoft.AspNetCore.Mvc.TagHelpers @model Passwordless.AspNetIdentity.Example.Pages.Account.Recovery + + @{ ViewData["Title"] = "Account Recovery"; } @@ -17,7 +23,10 @@ and a "magic link" will be used to authenticate the intended user. - +

Recovery Method:

+ + + @if (!string.IsNullOrWhiteSpace(Model.RecoveryMessage)) diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml.cs b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml.cs index da11fff..76b2c47 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml.cs +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Recovery.cshtml.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; @@ -22,6 +21,9 @@ public class Recovery : PageModel private readonly IUrlHelperFactory _urlHelperFactory; private readonly IActionContextAccessor _actionContextAccessor; + public const string MagicLink = "magic_link"; + public const string GeneratedSignIn = "generated_signin"; + public RecoveryForm Form { get; } = new(); public string RecoveryMessage { get; set; } = string.Empty; @@ -60,26 +62,41 @@ public async Task OnPostAsync(RecoveryForm form, CancellationToke throw new InvalidOperationException("ActionContext is null"); } - var token = await _passwordlessClient.GenerateAuthenticationTokenAsync(new AuthenticationOptions(user.Id), cancellationToken); var urlBuilder = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var url = urlBuilder.PageLink("/Account/Magic") ?? urlBuilder.Content("~/"); var uriBuilder = new UriBuilder(url); var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["token"] = token.Token; + query["token"] = string.Empty; uriBuilder.Query = query.ToString(); + var successMessage = string.Empty; + + if (string.Equals(form.RecoveryMethod, MagicLink, StringComparison.OrdinalIgnoreCase)) + { + await _passwordlessClient.SendMagicLinkAsync(new SendMagicLinkRequest(form.Email!, uriBuilder.Uri + "$TOKEN", user.Id, null), cancellationToken); + + successMessage = "Check your API magic link destination (mail.md if running local, email if using cloud."; + } + else if (string.Equals(form.RecoveryMethod, GeneratedSignIn, StringComparison.OrdinalIgnoreCase)) + { + var token = await _passwordlessClient.GenerateAuthenticationTokenAsync(new AuthenticationOptions(user.Id), cancellationToken); + query["token"] = token.Token; + uriBuilder.Query = query.ToString(); - var message = $""" - New message: - - Click the link to recover your account - Link - {Environment.NewLine} - """; + var message = $""" + New message: - await System.IO.File.AppendAllTextAsync("mail.md", message, cancellationToken); + This was generated with manually generated authentication token. + Link + {Environment.NewLine} + """; - return RedirectToPage("Recovery", "SuccessfulRecovery", new { message }); + await System.IO.File.AppendAllTextAsync("mail.md", message, cancellationToken); + + successMessage = message; + } + + return RedirectToPage("Recovery", "SuccessfulRecovery", new { message = successMessage }); } } @@ -87,5 +104,8 @@ public class RecoveryForm { [EmailAddress] [Required] - public string? Email { get; set; } + public string Email { get; set; } = string.Empty; + + [Required] + public string RecoveryMethod { get; set; } = string.Empty; } \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Register.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Register.cshtml index d879aef..57c77e8 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Register.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Register.cshtml @@ -36,8 +36,9 @@ @if (canAddPasskeys) { - } \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml new file mode 100644 index 0000000..99718e7 --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml @@ -0,0 +1,9 @@ +@page +@model Passwordless.AspNetIdentity.Example.Pages.Authorized.ElevatedAuthentication + +@{ + ViewData["Title"] = "Elevated Auth"; +} +

@ViewData["Title"]

+ +

Step up authentication successful.

\ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml.cs b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml.cs new file mode 100644 index 0000000..c4417c3 --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/ElevatedAuthentication.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Passwordless.AspNetIdentity.Example.Authorization; + +namespace Passwordless.AspNetIdentity.Example.Pages.Authorized; + +[Authorize(Policy = StepUpPurposes.StepUp)] +public class ElevatedAuthentication : PageModel +{ + public void OnGet() + { + } +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/HelloWorld.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/HelloWorld.cshtml index 1dde13b..3b61ed1 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/HelloWorld.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/HelloWorld.cshtml @@ -42,12 +42,27 @@ +
+

Current Claims:

+
    + @foreach (var claim in Model.Claims) + { +
  • @claim.ClaimName : @claim.ClaimValue
  • + } +
+
+
To Elevated Area @if (Model.CanAddPassKeys) { - + +

@ViewData["Title"]

+ +

Hello, @User.Identity!.Name

+ +

Please enter credentials to perform action.

+ + + + + +

This page requires an elevated authorization using the "@Model.RequestedContext" context from step-up.

\ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/Stepup.cshtml.cs b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/Stepup.cshtml.cs new file mode 100644 index 0000000..caf4aac --- /dev/null +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Authorized/Stepup.cshtml.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Passwordless.AspNetIdentity.Example.Pages.Authorized; + +public class StepUp : PageModel +{ + public AuthenticatedUserModel? AuthenticatedUser { get; private set; } + [BindProperty] public string RequestedContext { get; set; } = string.Empty; + [BindProperty] public string ReturnUrl { get; set; } = string.Empty; + [BindProperty] public string StepUpVerifyToken { get; set; } = string.Empty; + + public void OnGet(string context, string returnUrl) + { + var identity = HttpContext.User.Identity!; + var email = User.FindFirstValue(ClaimTypes.Email)!; + + AuthenticatedUser = new AuthenticatedUserModel(identity.Name!, email); + RequestedContext = context; + ReturnUrl = returnUrl; + } +} \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Shared/_Layout.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Shared/_Layout.cshtml index ac156db..08a5b97 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Shared/_Layout.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Shared/_Layout.cshtml @@ -60,7 +60,6 @@ - @await RenderSectionAsync("Scripts", required: false) diff --git a/examples/Passwordless.AspNetIdentity.Example/Program.cs b/examples/Passwordless.AspNetIdentity.Example/Program.cs index d93c93d..cad4c64 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Program.cs +++ b/examples/Passwordless.AspNetIdentity.Example/Program.cs @@ -1,11 +1,24 @@ +using System; +using System.Globalization; +using System.Net.Http; +using System.Net.Http.Json; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Passwordless; using Passwordless.AspNetCore; +using Passwordless.AspNetIdentity.Example.Authorization; using Passwordless.AspNetIdentity.Example.DataContext; var builder = WebApplication.CreateBuilder(args); @@ -16,6 +29,15 @@ .AddEntityFrameworkStores() .AddPasswordless(builder.Configuration.GetRequiredSection("Passwordless")); +builder.Services.ConfigureApplicationCookie(options => +{ + options.AccessDeniedPath = "/AccessDenied"; +}); + +builder.Services.AddAuthorizationBuilder() + .AddPolicy(StepUpPurposes.StepUp, policy => policy.AddRequirements([new StepUpRequirement(StepUpPurposes.StepUp)])); + +builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddRazorPages(options => @@ -23,6 +45,8 @@ options.Conventions.AuthorizeFolder("/Authorized"); }); +builder.Services.AddSingleton(); + var app = builder.Build(); // Execute our migrations to generate our `example.db` file with all the required tables. @@ -49,4 +73,68 @@ app.MapRazorPages(); app.MapControllers(); -app.Run(); \ No newline at end of file +app.MapGet("AccessDenied", AccessRedirect); +app.MapPost("stepup", StepUp); + +app.Run(); +return; + +static IResult AccessRedirect(string returnUrl, HttpContext context, StepUpPurpose stepUpContext) +{ + + if (context.User.Identity!.IsAuthenticated && !string.IsNullOrWhiteSpace(stepUpContext.Purpose)) + { + return Results.Redirect($"Authorized/StepUp?returnUrl={returnUrl}&context={stepUpContext.Purpose}"); + } + + return Results.Redirect("Account/Login"); +} + +static async Task StepUp(IOptions options, HttpContext context, StepUpPurpose stepUpContext, [FromBody] StepUpRequest request) +{ + var http = new HttpClient + { + BaseAddress = new Uri(options.Value.ApiUrl), + DefaultRequestHeaders = { { "ApiSecret", options.Value.ApiSecret } } + }; + + using var response = await http.PostAsJsonAsync("/signin/verify", new + { + Token = request.StepUpToken, + Purpose = request.Purpose + }); + + var token = await response.Content.ReadFromJsonAsync(); + + var identity = (ClaimsIdentity)context.User.Identity!; + var existingStepUpClaim = identity.FindFirst(request.Purpose); + + if (existingStepUpClaim != null) + { + identity.RemoveClaim(existingStepUpClaim); + } + identity.AddClaim(new Claim(request.Purpose, DateTime.UtcNow.Add(TimeSpan.FromMinutes(5)).ToString(CultureInfo.CurrentCulture))); + + stepUpContext.Purpose = string.Empty; + + await context.SignInAsync(IdentityConstants.ApplicationScheme, new ClaimsPrincipal(identity)); + + return Results.Redirect(request.ReturnUrl); +} + +record StepUpRequest(string StepUpToken, string ReturnUrl, string Purpose); + +record StepUpToken( + string UserId, + byte[] CredentialId, + bool Success, + DateTime Timestamp, + string RpId, + string Origin, + string Device, + string Country, + string Nickname, + DateTime ExpiresAt, + Guid TokenId, + string Type, + string Purpose) : VerifiedUser(UserId, CredentialId, Success, Timestamp, RpId, Origin, Device, Country, Nickname, ExpiresAt, TokenId, Type, Purpose); \ No newline at end of file diff --git a/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js b/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js index ac49c18..5f28270 100644 --- a/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js +++ b/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js @@ -1,4 +1 @@ -// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -// for details on configuring this project to bundle and minify static web assets. - -// Write your JavaScript code. + \ No newline at end of file diff --git a/src/Passwordless/Models/VerifiedUser.cs b/src/Passwordless/Models/VerifiedUser.cs index 27015fe..106b9ee 100644 --- a/src/Passwordless/Models/VerifiedUser.cs +++ b/src/Passwordless/Models/VerifiedUser.cs @@ -14,5 +14,6 @@ public record VerifiedUser( string Nickname, DateTime ExpiresAt, Guid TokenId, - string Type + string Type, + string Purpose ); \ No newline at end of file diff --git a/tests/Passwordless.AspNetCore.Tests/OldTests/PasswordlessServiceTests.cs b/tests/Passwordless.AspNetCore.Tests/OldTests/PasswordlessServiceTests.cs index 0842f81..ef04095 100644 --- a/tests/Passwordless.AspNetCore.Tests/OldTests/PasswordlessServiceTests.cs +++ b/tests/Passwordless.AspNetCore.Tests/OldTests/PasswordlessServiceTests.cs @@ -29,6 +29,25 @@ public class PasswordlessServiceTests private readonly Mock _mockServiceProvider = new(); private readonly Fixture _fixture = new(); + public PasswordlessServiceTests() + { + _fixture.Register(() => new VerifiedUser( + Guid.NewGuid().ToString(), + _fixture.Create(), + true, + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create(), + _fixture.Create())); + } + + private PasswordlessService CreateSut() { var mockOptions = new Mock>(); @@ -184,19 +203,8 @@ private static ClaimsPrincipal CreateClaimsPrincipal(Guid? userId, string? authe [Fact] public async Task LoginUserAsync_UsesDefaultSchemeIfNoneSpecified() { - var verifiedUser = new VerifiedUser( - Guid.NewGuid().ToString(), - _fixture.Create(), - true, - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create()); + var verifiedUser = _fixture.Create(); + var user = new TestUser { Id = Guid.Parse(verifiedUser.UserId), @@ -224,19 +232,9 @@ public async Task LoginUserAsync_UsesDefaultSchemeIfNoneSpecified() [Fact] public async Task LoginUserAsync_UsesOurOptionIfSpecified() { - var verifiedUser = new VerifiedUser( - Guid.NewGuid().ToString(), - _fixture.Create(), - true, - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create()); + + var verifiedUser = _fixture.Create(); + var user = new TestUser { Id = Guid.Parse(verifiedUser.UserId), @@ -266,19 +264,9 @@ public async Task LoginUserAsync_UsesOurOptionIfSpecified() [Fact] public async Task LoginUserAsync_TriesAuthenticationOptionsIfOursIsNull() { - var verifiedUser = new VerifiedUser( - Guid.NewGuid().ToString(), - _fixture.Create(), - true, - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create()); + + var verifiedUser = _fixture.Create(); + var user = new TestUser { Id = Guid.Parse(verifiedUser.UserId), @@ -315,19 +303,9 @@ public async Task LoginUserAsync_TriesAuthenticationOptionsIfOursIsNull() [Fact] public async Task LoginUserAsync_UserDoesNotExist_ReturnsUnauthorized() { - var verifiedUser = new VerifiedUser( - Guid.NewGuid().ToString(), - _fixture.Create(), - true, - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create()); + + var verifiedUser = _fixture.Create(); + _mockPasswordlessClient .Setup(s => s.VerifyAuthenticationTokenAsync("test_token", default)) .ReturnsAsync(verifiedUser); @@ -341,19 +319,8 @@ public async Task LoginUserAsync_UserDoesNotExist_ReturnsUnauthorized() [Fact] public async Task AddCredentialAsync_ReturnsRegisterTokenResponse() { - var verifiedUser = new VerifiedUser( - Guid.NewGuid().ToString(), - _fixture.Create(), - true, - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create(), - _fixture.Create()); + var verifiedUser = _fixture.Create(); + var user = new TestUser { Id = Guid.Parse(verifiedUser.UserId),