diff --git a/MicrosoftEntraIDMultiApis/MultiMicrosoftEntraIDWebApi/MultiMicrosoftEntraIDWebApi.csproj b/MicrosoftEntraIDMultiApis/MultiMicrosoftEntraIDWebApi/MultiMicrosoftEntraIDWebApi.csproj index d1364f0..ee550ab 100644 --- a/MicrosoftEntraIDMultiApis/MultiMicrosoftEntraIDWebApi/MultiMicrosoftEntraIDWebApi.csproj +++ b/MicrosoftEntraIDMultiApis/MultiMicrosoftEntraIDWebApi/MultiMicrosoftEntraIDWebApi.csproj @@ -7,10 +7,10 @@ - + - + diff --git a/MicrosoftEntraIDMultiApis/TestMultiApis/Pages/Shared/_Layout.cshtml b/MicrosoftEntraIDMultiApis/TestMultiApis/Pages/Shared/_Layout.cshtml index d76dda2..003a17e 100644 --- a/MicrosoftEntraIDMultiApis/TestMultiApis/Pages/Shared/_Layout.cshtml +++ b/MicrosoftEntraIDMultiApis/TestMultiApis/Pages/Shared/_Layout.cshtml @@ -44,7 +44,7 @@
- © 2025 - Razor Microsoft Entra ID + © 2026 - Razor Microsoft Entra ID
diff --git a/MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj b/MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj index 4d64fc7..0531e3d 100644 --- a/MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj +++ b/MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/IdentityHostingStartup.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/IdentityHostingStartup.cs index e7ccb35..b317425 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/IdentityHostingStartup.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/IdentityHostingStartup.cs @@ -1,5 +1,5 @@ -[assembly: HostingStartup(typeof(OpeniddictServer.Areas.Identity.IdentityHostingStartup))] -namespace OpeniddictServer.Areas.Identity +[assembly: HostingStartup(typeof(IdentityProvider.Areas.Identity.IdentityHostingStartup))] +namespace IdentityProvider.Areas.Identity { public class IdentityHostingStartup : IHostingStartup { diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml index 5f165ae..8d46dd5 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml @@ -1,39 +1,44 @@ @page +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Data +@using IdentityProvider.Passkeys @model LoginModel @{ ViewData["Title"] = "Log in"; } +

@ViewData["Title"]

-

Use a local account to log in.

+

Use a local account to log in.


-
- -
- - -
We'll never share your email with anyone else.
+ +
+ +
-
- - +
+ +
-
- - +
+
-
-
+ +
+ + + Log in with a passkey + +
@@ -57,8 +69,10 @@ {

- There are no external authentication services configured. See this article - about setting up this ASP.NET application to support logging in via external services. + There are no external authentication services configured. See this + article + about setting up this ASP.NET application to support logging in via external services + .

} @@ -67,9 +81,9 @@

- @foreach (var provider in Model.ExternalLogins) + @foreach (var provider in Model.ExternalLogins!) { - + }

@@ -81,6 +95,7 @@
@section Scripts { - - + + } + diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml.cs index ed13c11..7d9e054 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -2,143 +2,142 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable -using Fido2Identity; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OpeniddictServer.Data; +using IdentityProvider.Data; using System.ComponentModel.DataAnnotations; -namespace OpeniddictServer.Areas.Identity.Pages.Account +namespace IdentityProvider.Areas.Identity.Pages.Account; + +[AllowAnonymous] +public class LoginModel : PageModel { - public class LoginModel : PageModel - { - private readonly SignInManager _signInManager; - private readonly Fido2Store _fido2Store; - private readonly ILogger _logger; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; - public LoginModel(SignInManager signInManager, - Fido2Store fido2Store, - ILogger logger) - { - _signInManager = signInManager; - _fido2Store = fido2Store; - _logger = logger; - } + public LoginModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [BindProperty] + public InputModel Input { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public IList ExternalLogins { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public string ReturnUrl { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [TempData] + public string ErrorMessage { get; set; } + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InputModel + { /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - [BindProperty] - public InputModel Input { get; set; } + [Required] + [EmailAddress] + public string Email { get; set; } /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public IList ExternalLogins { get; set; } + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public string ReturnUrl { get; set; } + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [TempData] - public string ErrorMessage { get; set; } + public PasskeyInputModel Passkey { get; set; } + } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public class InputModel + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) { - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [Required] - [EmailAddress] - public string Email { get; set; } - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [Required] - [DataType(DataType.Password)] - public string Password { get; set; } - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } + ModelState.AddModelError(string.Empty, ErrorMessage); } - public async Task OnGetAsync(string returnUrl = null) - { - if (!string.IsNullOrEmpty(ErrorMessage)) - { - ModelState.AddModelError(string.Empty, ErrorMessage); - } + returnUrl ??= Url.Content("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl ??= Url.Content("~/"); - returnUrl ??= Url.Content("~/"); + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); - // Clear the existing external cookie to ensure a clean login process - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + Microsoft.AspNetCore.Identity.SignInResult result = null; - ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson)) + { + // When performing passkey sign-in, don't perform form validation. + ModelState.Clear(); - ReturnUrl = returnUrl; + result = await _signInManager.PasskeySignInAsync(Input.Passkey.CredentialJson); + } + else if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); } - public async Task OnPostAsync(string returnUrl = null) + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else { - returnUrl ??= Url.Content("~/"); - - ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); - - if (ModelState.IsValid) - { - // This doesn't count login failures towards account lockout - // To enable password failures to trigger account lockout, set lockoutOnFailure: true - var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); - if (result.Succeeded) - { - _logger.LogInformation("User logged in."); - return LocalRedirect(returnUrl); - } - if (result.RequiresTwoFactor) - { - var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(Input.Email); - if (fido2ItemExistsForUser.Count > 0) - { - return RedirectToPage("./LoginFido2Mfa", new { ReturnUrl = returnUrl, Input.RememberMe }); - } - - return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); - } - if (result.IsLockedOut) - { - _logger.LogWarning("User account locked out."); - return RedirectToPage("./Lockout"); - } - else - { - ModelState.AddModelError(string.Empty, "Invalid login attempt."); - return Page(); - } - } - - // If we got this far, something failed, redisplay form + ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); } } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml index 3c29515..4a931fe 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml @@ -1,5 +1,11 @@ @page +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Data +@using IdentityProvider.Passkeys @using Microsoft.AspNetCore.Identity @inject SignInManager SignInManager @inject UserManager UserManager @@ -10,7 +16,7 @@ return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken; } } -@model OpeniddictServer.Areas.Identity.Pages.Account.MfaModel +@model IdentityProvider.Areas.Identity.Pages.Account.MfaModel @{ ViewData["Title"] = "Login with Fido2 MFA"; } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml.cs index 8f7dba6..70a6823 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -namespace OpeniddictServer.Areas.Identity.Pages.Account; +namespace IdentityProvider.Areas.Identity.Pages.Account; [AllowAnonymous] public class MfaModel : PageModel diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 0000000..0033d05 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,36 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Change password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

@ViewData["Title"]

+ +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+ + +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 0000000..e5f9853 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class ChangePasswordModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ChangePasswordModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } = string.Empty; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = string.Empty; + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToPage("./SetPassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + foreach (var error in changePasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToPage(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml new file mode 100644 index 0000000..173ffc4 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml @@ -0,0 +1,33 @@ +@page +@model DeletePersonalDataModel +@{ + ViewData["Title"] = "Delete Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ + + +
+
+
+ @if (Model.RequirePassword) + { +
+ + + +
+ } + +
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs new file mode 100644 index 0000000..960aa84 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class DeletePersonalDataModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public DeletePersonalDataModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; + } + + public bool RequirePassword { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + RequirePassword = await _userManager.HasPasswordAsync(user); + if (RequirePassword) + { + if (!await _userManager.CheckPasswordAsync(user, Input.Password)) + { + ModelState.AddModelError(string.Empty, "Incorrect password."); + return Page(); + } + } + + var result = await _userManager.DeleteAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'."); + } + + await _signInManager.SignOutAsync(); + + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + return Redirect("~/"); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml index 31ecb7e..2b84b9a 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml @@ -14,12 +14,12 @@

Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key - used in an authenticator app you should reset your authenticator keys. + used in an authenticator app you should reset your authenticator keys.

-
+
diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs index 817a15b..1f62306 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -1,74 +1,58 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using Fido2Identity; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OpeniddictServer.Data; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; -namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage +public class Disable2faModel : PageModel { - public class Disable2faModel : PageModel + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public Disable2faModel( + UserManager userManager, + ILogger logger) { - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly Fido2Store _fido2Store; + _userManager = userManager; + _logger = logger; + } - public Disable2faModel( - UserManager userManager, - Fido2Store fido2Store, - ILogger logger) + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) { - _userManager = userManager; - _fido2Store = fido2Store; - _logger = logger; + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [TempData] - public string StatusMessage { get; set; } - - public async Task OnGet() + if (!await _userManager.GetTwoFactorEnabledAsync(user)) { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } + throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); + } - if (!await _userManager.GetTwoFactorEnabledAsync(user)) - { - throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled."); - } + return Page(); + } - return Page(); + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - public async Task OnPostAsync() + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - // remove Fido2 MFA if it exists - await _fido2Store.RemoveCredentialsByUserNameAsync(user.UserName); - - var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); - if (!disable2faResult.Succeeded) - { - throw new InvalidOperationException($"Unexpected error occurred disabling 2FA."); - } - - _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); - StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; - return RedirectToPage("./TwoFactorAuthentication"); + throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); } + + _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); + StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; + return RedirectToPage("./TwoFactorAuthentication"); } } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml new file mode 100644 index 0000000..35db45c --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml @@ -0,0 +1,12 @@ +@page +@model DownloadPersonalDataModel +@{ + ViewData["Title"] = "Download Your Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +@section Scripts { + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs new file mode 100644 index 0000000..049b142 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.Text.Json; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class DownloadPersonalDataModel : PageModel +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public DownloadPersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(ApplicationUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await _userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + Response.Headers.Append("Content-Disposition", "attachment; filename=PersonalData.json"); + return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json"); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml new file mode 100644 index 0000000..07628f5 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml @@ -0,0 +1,43 @@ +@page +@model EmailModel +@{ + ViewData["Title"] = "Manage Email"; + ViewData["ActivePage"] = ManageNavPages.Email; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + @if (Model.IsEmailConfirmed) + { +
+ +
+ +
+
+ } + else + { + + + } +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs new file mode 100644 index 0000000..2e66d42 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs @@ -0,0 +1,143 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public partial class EmailModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + + public EmailModel( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + } + + public string Username { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public bool IsEmailConfirmed { get; set; } + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string NewEmail { get; set; } = string.Empty; + } + + private async Task LoadAsync(ApplicationUser user) + { + var email = await _userManager.GetEmailAsync(user); + Email = email!; + + Input = new InputModel + { + NewEmail = email!, + }; + + IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync(user); + return Page(); + } + + public async Task OnPostChangeEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var email = await _userManager.GetEmailAsync(user); + if (Input.NewEmail != email) + { + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmailChange", + pageHandler: null, + values: new { userId = userId, email = Input.NewEmail, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + Input.NewEmail, + "Confirm your email", + $"Please confirm your account by clicking here."); + + StatusMessage = "Confirmation link to change email sent. Please check your email."; + return RedirectToPage(); + } + + StatusMessage = "Your email is unchanged."; + return RedirectToPage(); + } + + public async Task OnPostSendVerificationEmailAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var userId = await _userManager.GetUserIdAsync(user); + var email = await _userManager.GetEmailAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId, code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + email!, + "Confirm your email", + $"Please confirm your account by clicking here."); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToPage(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000..07893a0 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,56 @@ +@page +@model EnableAuthenticatorModel +@{ + ViewData["Title"] = "Configure authenticator app"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + + + + +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 0000000..382c749 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,150 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class EnableAuthenticatorModel : PageModel +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public EnableAuthenticatorModel( + UserManager userManager, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _logger = logger; + _urlEncoder = urlEncoder; + } + + public string SharedKey { get; set; } = string.Empty; + + public string AuthenticatorUri { get; set; } = string.Empty; + + [TempData] + public string[] RecoveryCodes { get; set; } = []; + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } = string.Empty; + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadSharedKeyAndQrCodeUriAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + // Strip spaces and hypens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("Input.Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + StatusMessage = "Your authenticator app has been verified."; + + if (await _userManager.CountRecoveryCodesAsync(user) == 0) + { + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes!.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); + } + else + { + return RedirectToPage("./TwoFactorAuthentication"); + } + } + + private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + SharedKey = FormatKey(unformattedKey!); + + var email = await _userManager.GetEmailAsync(user); + AuthenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenticatorUriFormat, + _urlEncoder.Encode("AheadIdentityProvider"), + _urlEncoder.Encode(email), + unformattedKey); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000..203fb22 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml @@ -0,0 +1,56 @@ +@page +@model ExternalLoginsModel +@{ + ViewData["Title"] = "Manage your external logins"; + ViewData["ActivePage"] = ManageNavPages.ExternalLogins; +} + + +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+ +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs new file mode 100644 index 0000000..a3b512f --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class ExternalLoginsModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ExternalLoginsModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentLogins { get; set; } = []; + + public IList OtherLogins { get; set; } = []; + + public bool ShowRemoveButton { get; set; } + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + CurrentLogins = await _userManager.GetLoginsAsync(user); + OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + return Page(); + } + + public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (!result.Succeeded) + { + StatusMessage = "The external login was not removed."; + return RedirectToPage(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "The external login was removed."; + return RedirectToPage(); + } + + public async Task OnPostLinkLoginAsync(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetLinkLoginCallbackAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID 'user.Id'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); + if (info == null) + { + throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + StatusMessage = "The external login was not added. External logins can only be associated with one account."; + return RedirectToPage(); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToPage(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml deleted file mode 100644 index 331500f..0000000 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml +++ /dev/null @@ -1,62 +0,0 @@ -@page "/Fido2Mfa/{handler?}" -@using Microsoft.AspNetCore.Identity -@inject SignInManager SignInManager -@inject UserManager UserManager -@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf -@functions{ - public string? GetAntiXsrfRequestToken() - { - return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken; - } -} -@model OpeniddictServer.Areas.Identity.Pages.Account.Manage.MfaModel -@{ - Layout = "_Layout.cshtml"; - ViewData["Title"] = "Two-factor authentication (2FA)"; - ViewData["ActivePage"] = ManageNavPages.Fido2Mfa; -} - -

@ViewData["Title"]

-
-
-

2FA/MFA

-

This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.

- - -
-
- -

Add a Fido2 MFA

-
- -
- -
- -
-
- -
-
- -
-
-
-
-
- - -
- -
-
- - - - - - - - \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml.cs deleted file mode 100644 index c83d168..0000000 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage; - -public class MfaModel : PageModel -{ - public void OnGet() - { - } - - public void OnPost() - { - } -} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000..14efa7d --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,27 @@ +@page +@model GenerateRecoveryCodesModel +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..999bacd --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class GenerateRecoveryCodesModel : PageModel +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public GenerateRecoveryCodesModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + [TempData] + public string[] RecoveryCodes { get; set; } = []; + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes!.ToArray(); + + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + StatusMessage = "You have generated new recovery codes."; + return RedirectToPage("./ShowRecoveryCodes"); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml new file mode 100644 index 0000000..b317056 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,42 @@ +@page +@model IndexModel +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Areas.Identity.Pages.Account.Manage +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@using Microsoft.AspNetCore.Identity +@inject UserManager UserManager +@{ + ViewData["Title"] = "Profile"; + ViewData["ActivePage"] = ManageNavPages.Index; + + var user = await UserManager.GetUserAsync(User); +} + +

@ViewData["Title"]

+ +
+
+
+
+ +
+ + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 0000000..45a164b --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public partial class IndexModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public IndexModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public string Username { get; set; } = string.Empty; + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + public class InputModel + { + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } = string.Empty; + } + + private async Task LoadAsync(ApplicationUser user) + { + var userName = await _userManager.GetUserNameAsync(user); + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + + Username = userName!; + + Input = new InputModel + { + PhoneNumber = phoneNumber! + }; + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadAsync(user); + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadAsync(user); + return Page(); + } + + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + if (Input.PhoneNumber != phoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + StatusMessage = "Unexpected error when trying to set phone number."; + return RedirectToPage(); + } + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your profile has been updated"; + return RedirectToPage(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs index a5ed650..7f609f9 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -1,126 +1,45 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#nullable disable +using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.Rendering; +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; -namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage +public static class ManageNavPages { - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static class ManageNavPages + public static string Index => "Index"; + + public static string Email => "Email"; + + public static string ChangePassword => "ChangePassword"; + + public static string DownloadPersonalData => "DownloadPersonalData"; + + public static string DeletePersonalData => "DeletePersonalData"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string PersonalData => "PersonalData"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + private static string PageNavClass(ViewContext viewContext, string page) { - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string Index => "Index"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string Email => "Email"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ChangePassword => "ChangePassword"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DownloadPersonalData => "DownloadPersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DeletePersonalData => "DeletePersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ExternalLogins => "ExternalLogins"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PersonalData => "PersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string TwoFactorAuthentication => "TwoFactorAuthentication"; - - public static string Fido2Mfa => "Fido2Mfa"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); - - public static string Fido2MfaNavClass(ViewContext viewContext) => PageNavClass(viewContext, Fido2Mfa); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PageNavClass(ViewContext viewContext, string page) - { - var activePage = viewContext.ViewData["ActivePage"] as string - ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); - return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; - } + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty; } } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml new file mode 100644 index 0000000..4de320c --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml @@ -0,0 +1,27 @@ +@page +@model PersonalDataModel +@{ + ViewData["Title"] = "Personal Data"; + ViewData["ActivePage"] = ManageNavPages.PersonalData; +} + +

@ViewData["Title"]

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ +
+

+ Delete +

+
+
+ +@section Scripts { + +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs new file mode 100644 index 0000000..9eb8473 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class PersonalDataModel : PageModel +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public PersonalDataModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000..081c824 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,24 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; +} + + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 0000000..c69df3a --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class ResetAuthenticatorModel : PageModel +{ + UserManager _userManager; + private readonly SignInManager _signInManager; + ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; + + return RedirectToPage("./EnableAuthenticator"); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 0000000..aa8433a --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,35 @@ +@page +@model SetPasswordModel +@{ + ViewData["Title"] = "Set password"; + ViewData["ActivePage"] = ManageNavPages.ChangePassword; +} + +

Set your password

+ +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 0000000..17f0219 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.ComponentModel.DataAnnotations; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class SetPasswordModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public SetPasswordModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [BindProperty] + public InputModel Input { get; set; } = new InputModel(); + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = string.Empty; + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToPage("./ChangePassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); + if (!addPasswordResult.Succeeded) + { + foreach (var error in addPasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.RefreshSignInAsync(user); + StatusMessage = "Your password has been set."; + + return RedirectToPage(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000..c13a2fb --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000..201e06d --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; + +public class ShowRecoveryCodesModel : PageModel +{ + [TempData] + public string[] RecoveryCodes { get; set; } = []; + + [TempData] + public string StatusMessage { get; set; } = string.Empty; + + public IActionResult OnGet() + { + if (RecoveryCodes == null || RecoveryCodes.Length == 0) + { + return RedirectToPage("./TwoFactorAuthentication"); + } + + return Page(); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml index 9ceade8..83dacef 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -1,5 +1,4 @@ @page -@using Microsoft.AspNetCore.Http.Features @model TwoFactorAuthenticationModel @{ ViewData["Title"] = "Two-factor authentication (2FA)"; @@ -8,67 +7,53 @@

@ViewData["Title"]

-@{ - var consentFeature = HttpContext.Features.Get(); - @if (consentFeature?.CanTrack ?? true) +@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) { - @if (Model.Is2faEnabled) - { - if (Model.RecoveryCodesLeft == 0) - { -
- You have no recovery codes left. -

You must generate a new set of recovery codes before you can log in with a recovery code.

-
- } - else if (Model.RecoveryCodesLeft == 1) - { -
- You have 1 recovery code left. -

You can generate a new set of recovery codes.

-
- } - else if (Model.RecoveryCodesLeft <= 3) - { -
- You have @Model.RecoveryCodesLeft recovery codes left. -

You should generate a new set of recovery codes.

-
- } - - if (Model.IsMachineRemembered) - { -
- -
- } - Disable 2FA - Reset recovery codes - } - -

Authenticator app

- @if (!Model.HasAuthenticator) - { - Add authenticator app - } - else - { - Set up authenticator app - Reset authenticator app - } +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
} - else + else if (Model.RecoveryCodesLeft == 1) {
- Privacy and cookie policy have not been accepted. -

You must accept the policy before you can enable two factor authentication.

+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

} + + if (Model.IsMachineRemembered) + { +
+ +
+ } + Disable 2FA + Reset recovery codes } -Add Fido2 MFA +

Authenticator app

+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Setup authenticator app + Reset authenticator app +} + +Setup passkeys @section Scripts { - - -} + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs index c1a7fe6..91c3022 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -1,85 +1,66 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OpeniddictServer.Data; +using IdentityProvider.Data; + +namespace IdentityProvider.Areas.Identity.Pages.Account.Manage; -namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage +public class TwoFactorAuthenticationModel : PageModel { - public class TwoFactorAuthenticationModel : PageModel - { - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; - public TwoFactorAuthenticationModel( - UserManager userManager, SignInManager signInManager) - { - _userManager = userManager; - _signInManager = signInManager; - } + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public TwoFactorAuthenticationModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public bool HasAuthenticator { get; set; } + public bool HasAuthenticator { get; set; } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public int RecoveryCodesLeft { get; set; } + public int RecoveryCodesLeft { get; set; } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [BindProperty] - public bool Is2faEnabled { get; set; } + [BindProperty] + public bool Is2faEnabled { get; set; } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public bool IsMachineRemembered { get; set; } + public bool IsMachineRemembered { get; set; } - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - [TempData] - public string StatusMessage { get; set; } + [TempData] + public string StatusMessage { get; set; } = string.Empty; - public async Task OnGetAsync() + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } - HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; - Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); - IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); - RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; + Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); - return Page(); - } + return Page(); + } - public async Task OnPostAsync() + public async Task OnPost() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - await _signInManager.ForgetTwoFactorClientAsync(); - StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; - return RedirectToPage(); + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } + + await _signInManager.ForgetTwoFactorClientAsync(); + StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; + return RedirectToPage(); } } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml index d58ebc4..e2f2f67 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -1,15 +1,34 @@ -@inject SignInManager SignInManager +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Areas.Identity.Pages.Account.Manage +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@using Microsoft.AspNetCore.Identity +@inject SignInManager SignInManager +@inject UserManager UserManager @{ var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); + var user = await UserManager.GetUserAsync(User); + var notEntraIDUserWithNoPassword = await UserManager.HasPasswordAsync(user!); } diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000..208a424 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml index cb8f362..646fd85 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -1 +1,7 @@ -@using OpeniddictServer.Areas.Identity.Pages.Account.Manage +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Areas.Identity.Pages.Account.Manage +@using IdentityProvider.Data +@using IdentityProvider.Passkeys diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml new file mode 100644 index 0000000..d500546 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml @@ -0,0 +1,49 @@ +@page +@using System.Buffers.Text +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@model IdentityProvider.Areas.Identity.Pages.Account.PasskeysModel +@{ + ViewData["Title"] = "Manage your passkeys"; +} + + +

@ViewData["Title"]

+ +@if (Model.CurrentPasskeys?.Count > 0) +{ + + + @foreach (var passkey in Model.CurrentPasskeys) + { + var credentialId = Base64Url.EncodeToString(passkey.CredentialId); + + + + + } + +
@(passkey.Name ?? "Unnamed passkey") +
+ + + +
+
+} +else +{ +

No passkeys are registered.

+} + +
+ Add a new passkey +
+ +@section Scripts { + +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml.cs new file mode 100644 index 0000000..44a29d9 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Passkeys.cshtml.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using IdentityProvider.Data; +using System.Buffers.Text; +using System.Diagnostics.CodeAnalysis; + +namespace IdentityProvider.Areas.Identity.Pages.Account; + +public class PasskeysModel : PageModel +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public PasskeysModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentPasskeys { get; set; } + + [BindProperty] + public InputModel? Input { get; set; } + + public class InputModel + { + public string? CredentialId { get; set; } + + public string? Action { get; set; } + + public PasskeyInputModel? Passkey { get; set; } + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + CurrentPasskeys = await _userManager.GetPasskeysAsync(user); + return Page(); + } + + public async Task OnPostUpdatePasskeyAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (string.IsNullOrEmpty(Input?.CredentialId)) + { + StatusMessage = "Could not find the passkey."; + return RedirectToPage(); + } + + byte[] credentialId; + try + { + credentialId = Base64Url.DecodeFromChars(Input.CredentialId); + } + catch (FormatException) + { + StatusMessage = "The specified passkey ID had an invalid format."; + return RedirectToPage(); + } + + switch (Input?.Action) + { + case "rename": + return RedirectToPage("./RenamePasskey", new { id = Input.CredentialId }); + case "delete": + return await DeletePasskey(user, credentialId); + default: + StatusMessage = "Unknown action."; + return RedirectToPage(); + } + } + + private async Task DeletePasskey([NotNull] ApplicationUser user, byte[] credentialId) + { + var result = await _userManager.RemovePasskeyAsync(user, credentialId); + if (!result.Succeeded) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred removing passkey for user with ID '{userId}'."); + } + + StatusMessage = "The passkey was removed."; + return RedirectToPage(); + } + + public async Task OnPostAddPasskeyAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!string.IsNullOrEmpty(Input?.Passkey?.Error)) + { + StatusMessage = $"Could not add a passkey: {Input.Passkey.Error}"; + return RedirectToPage(); + } + + if (string.IsNullOrEmpty(Input?.Passkey?.CredentialJson)) + { + StatusMessage = "The browser did not provide a passkey."; + return RedirectToPage(); + } + + var attestationResult = await _signInManager.PerformPasskeyAttestationAsync(Input.Passkey.CredentialJson); + if (!attestationResult.Succeeded) + { + StatusMessage = $"Could not add the passkey: {attestationResult.Failure.Message}."; + return RedirectToPage(); + } + + var setPasskeyResult = await _userManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey); + if (!setPasskeyResult.Succeeded) + { + StatusMessage = "The passkey could not be added to your account."; + return RedirectToPage(); + } + + // Immediately prompt the user to enter a name for the credential + StatusMessage = "The passkey was added to your account. You can now use it to sign in. Give it an easy to remember name."; + return RedirectToPage("./RenamePasskey", new { id = Base64Url.EncodeToString(attestationResult.Passkey.CredentialId) }); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml new file mode 100644 index 0000000..f32534b --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml @@ -0,0 +1,34 @@ +@page +@model IdentityProvider.Areas.Identity.Pages.Account.RenamePasskeyModel +@{ + ViewData["Title"] = "Rename passkey"; +} + + + +@if (Model.Input?.Name is { } name) +{ +

Enter a new name for your "@name" passkey

+} +else +{ +

Enter a name for your passkey

+} + +
+ +
+ + + +
+
+ +
+
+ +@section Scripts { + +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml.cs b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml.cs new file mode 100644 index 0000000..af7b7d1 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/RenamePasskey.cshtml.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using IdentityProvider.Data; +using System.Buffers.Text; + +namespace IdentityProvider.Areas.Identity.Pages.Account; + +public class RenamePasskeyModel : PageModel +{ + private readonly UserManager _userManager; + private readonly ApplicationDbContext _dbContext; + + public RenamePasskeyModel( + UserManager userManager, + ApplicationDbContext dbContext) + { + _userManager = userManager; + _dbContext = dbContext; + } + + [BindProperty] public InputModel? Input { get; set; } + + public class InputModel + { + public string? CredentialId { get; set; } + + public string? Name { get; set; } + } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync(string id) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + byte[] credentialId; + try + { + credentialId = Base64Url.DecodeFromChars(id); + } + catch (FormatException) + { + StatusMessage = "The specified passkey ID had an invalid format."; + return RedirectToPage("./Passkeys"); + } + + var passkey = await _userManager.GetPasskeyAsync(user, credentialId); + if (passkey == null) + { + return NotFound($"Unable to load passkey ID '{_userManager.GetUserId(User)}'."); + } + + Input = new InputModel + { + CredentialId = id, + Name = passkey.Name + }; + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + byte[] credentialId; + try + { + credentialId = Base64Url.DecodeFromChars(Input?.CredentialId); + } + catch (FormatException) + { + StatusMessage = "The specified passkey ID had an invalid format."; + return RedirectToPage("./Passkeys"); + } + + var passkey = await _userManager.GetPasskeyAsync(user, credentialId); + if (passkey == null) + { + return NotFound($"Unable to load passkey ID '{_userManager.GetUserId(User)}'."); + } + + // Rename + passkey.Name = Input?.Name; + + var result = await _userManager.AddOrUpdatePasskeyAsync(user, passkey); + if (!result.Succeeded) + { + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred removing passkey for user with ID '{userId}'."); + } + + // REVIEW: Only one of the .NET 10 user stores update the Name property of the passkey. Doing direct database access here to ensure the name is stored. + var passkeyEntity = await _dbContext.UserPasskeys.SingleOrDefaultAsync(userPasskey => userPasskey.CredentialId.SequenceEqual(credentialId)); + if (passkeyEntity != null) + { + passkeyEntity.Data.Name = Input?.Name; + await _dbContext.SaveChangesAsync(); + } + + StatusMessage = "The passkey was updated."; + return RedirectToPage("./Passkeys"); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/_ViewImports.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/_ViewImports.cshtml index c63caa3..68c3b23 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/_ViewImports.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -1 +1,6 @@ -@using OpeniddictServer.Areas.Identity.Pages.Account \ No newline at end of file +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Areas.Identity.Pages.Account +@using IdentityProvider.Data +@using IdentityProvider.Passkeys diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml index 58713e7..5d1f685 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -1,18 +1,2 @@ - - - - - - - - + + diff --git a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ViewImports.cshtml b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ViewImports.cshtml index d05cc44..3e4ac31 100644 --- a/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ViewImports.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/_ViewImports.cshtml @@ -1,5 +1,8 @@ -@using Microsoft.AspNetCore.Identity -@using OpeniddictServer.Areas.Identity -@using OpeniddictServer.Areas.Identity.Pages +@using IdentityProvider +@using IdentityProvider.Areas.Identity +@using IdentityProvider.Areas.Identity.Pages +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@using Microsoft.AspNetCore.Identity @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@using OpeniddictServer.Data +@addTagHelper *, IdentityProvider diff --git a/MultiIdentityProvider/IdentityProvider/Controllers/AuthorizationController.cs b/MultiIdentityProvider/IdentityProvider/Controllers/AuthorizationController.cs index d2007d7..573e152 100644 --- a/MultiIdentityProvider/IdentityProvider/Controllers/AuthorizationController.cs +++ b/MultiIdentityProvider/IdentityProvider/Controllers/AuthorizationController.cs @@ -13,13 +13,13 @@ using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; -using OpeniddictServer.Data; -using OpeniddictServer.Helpers; -using OpeniddictServer.ViewModels.Authorization; +using IdentityProvider.Data; +using IdentityProvider.Helpers; +using IdentityProvider.ViewModels.Authorization; using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace OpeniddictServer.Controllers; +namespace IdentityProvider.Controllers; public class AuthorizationController : Controller { @@ -304,44 +304,49 @@ public async Task Exchange() // Note: the client credentials are automatically validated by OpenIddict: // if client_id or client_secret are invalid, this action won't be invoked. - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - if (application == null) - { - throw new InvalidOperationException("The application details cannot be found in the database."); - } + return await HandleExchangeClientCredentialsGrantType(request); + } - // Create the claims-based identity that will be used by OpenIddict to generate tokens. - var identity = new ClaimsIdentity( - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: Claims.Name, - roleType: Claims.Role); - - // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier). - identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application)); - identity.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application)); - - // Note: In the original OAuth 2.0 specification, the client credentials grant - // doesn't return an identity token, which is an OpenID Connect concept. - // - // As a non-standardized extension, OpenIddict allows returning an id_token - // to convey information about the client application when the "openid" scope - // is granted (i.e specified when calling principal.SetScopes()). When the "openid" - // scope is not explicitly set, no identity token is returned to the client application. - - // Set the list of scopes granted to the client application in access_token. - var principal = new ClaimsPrincipal(identity); - principal.SetScopes(request.GetScopes()); - principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); - - foreach (var claim in principal.Claims) - { - claim.SetDestinations(GetDestinations(claim, principal)); - } + throw new InvalidOperationException("The specified grant type is not supported."); + } - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + private async Task HandleExchangeClientCredentialsGrantType(OpenIddictRequest request) + { + var application = await _applicationManager.FindByClientIdAsync(request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application details cannot be found in the database."); } - throw new InvalidOperationException("The specified grant type is not supported."); + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier). + identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application)); + identity.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application)); + + // Note: In the original OAuth 2.0 specification, the client credentials grant + // doesn't return an identity token, which is an OpenID Connect concept. + // + // As a non-standardized extension, OpenIddict allows returning an id_token + // to convey information about the client application when the "openid" scope + // is granted (i.e specified when calling principal.SetScopes()). When the "openid" + // scope is not explicitly set, no identity token is returned to the client application. + + // Set the list of scopes granted to the client application in access_token. + var principal = new ClaimsPrincipal(identity); + principal.SetScopes(request.GetScopes()); + principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } + + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } private async Task HandleExchangeCodeGrantType() diff --git a/MultiIdentityProvider/IdentityProvider/Controllers/ErrorController.cs b/MultiIdentityProvider/IdentityProvider/Controllers/ErrorController.cs index c6d3513..9cfdde2 100644 --- a/MultiIdentityProvider/IdentityProvider/Controllers/ErrorController.cs +++ b/MultiIdentityProvider/IdentityProvider/Controllers/ErrorController.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Mvc; -using OpeniddictServer.ViewModels.Shared; +using IdentityProvider.ViewModels.Shared; -namespace OpeniddictServer.Controllers; +namespace IdentityProvider.Controllers; public class ErrorController : Controller { diff --git a/MultiIdentityProvider/IdentityProvider/Controllers/HomeController.cs b/MultiIdentityProvider/IdentityProvider/Controllers/HomeController.cs index e01fd78..32fb0da 100644 --- a/MultiIdentityProvider/IdentityProvider/Controllers/HomeController.cs +++ b/MultiIdentityProvider/IdentityProvider/Controllers/HomeController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace OpeniddictServer.Controllers; +namespace IdentityProvider.Controllers; public class HomeController : Controller { diff --git a/MultiIdentityProvider/IdentityProvider/Controllers/ResourceController.cs b/MultiIdentityProvider/IdentityProvider/Controllers/ResourceController.cs index bd452e0..94f1e33 100644 --- a/MultiIdentityProvider/IdentityProvider/Controllers/ResourceController.cs +++ b/MultiIdentityProvider/IdentityProvider/Controllers/ResourceController.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using OpenIddict.Validation.AspNetCore; -using OpeniddictServer.Data; +using IdentityProvider.Data; -namespace OpeniddictServer.Controllers; +namespace IdentityProvider.Controllers; [Route("api")] public class ResourceController : Controller diff --git a/MultiIdentityProvider/IdentityProvider/Controllers/UserinfoController.cs b/MultiIdentityProvider/IdentityProvider/Controllers/UserinfoController.cs index 242265b..5c8ee58 100644 --- a/MultiIdentityProvider/IdentityProvider/Controllers/UserinfoController.cs +++ b/MultiIdentityProvider/IdentityProvider/Controllers/UserinfoController.cs @@ -4,10 +4,10 @@ using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; -using OpeniddictServer.Data; +using IdentityProvider.Data; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace OpeniddictServer.Controllers; +namespace IdentityProvider.Controllers; public class UserinfoController : Controller { diff --git a/MultiIdentityProvider/IdentityProvider/Data/ApplicationDbContext.cs b/MultiIdentityProvider/IdentityProvider/Data/ApplicationDbContext.cs index 20dc523..a1578d9 100644 --- a/MultiIdentityProvider/IdentityProvider/Data/ApplicationDbContext.cs +++ b/MultiIdentityProvider/IdentityProvider/Data/ApplicationDbContext.cs @@ -1,8 +1,7 @@ -using Fido2Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace OpeniddictServer.Data; +namespace IdentityProvider.Data; public class ApplicationDbContext : IdentityDbContext { @@ -11,12 +10,9 @@ public ApplicationDbContext(DbContextOptions options) { } - public DbSet FidoStoredCredential => Set(); - - protected override void OnModelCreating(ModelBuilder builder) + // Override to include passkey model + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { - builder.Entity().HasKey(m => m.Id); - - base.OnModelCreating(builder); + base.ConfigureConventions(configurationBuilder); } } \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Data/ApplicationUser.cs b/MultiIdentityProvider/IdentityProvider/Data/ApplicationUser.cs index 6a2dd70..4a67f37 100644 --- a/MultiIdentityProvider/IdentityProvider/Data/ApplicationUser.cs +++ b/MultiIdentityProvider/IdentityProvider/Data/ApplicationUser.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace OpeniddictServer.Data; +namespace IdentityProvider.Data; // Add profile data for application users by adding properties to the ApplicationUser class public class ApplicationUser : IdentityUser { } diff --git a/MultiIdentityProvider/IdentityProvider/Data/PasskeyInputModel.cs b/MultiIdentityProvider/IdentityProvider/Data/PasskeyInputModel.cs new file mode 100644 index 0000000..bba8c40 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Data/PasskeyInputModel.cs @@ -0,0 +1,7 @@ +namespace IdentityProvider.Data; + +public class PasskeyInputModel +{ + public string? CredentialJson { get; set; } + public string? Error { get; set; } +} diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/Fido2Store.cs b/MultiIdentityProvider/IdentityProvider/Fido2/Fido2Store.cs deleted file mode 100644 index 85b16c2..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/Fido2Store.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Fido2NetLib; -using Microsoft.EntityFrameworkCore; -using OpeniddictServer.Data; -using System.Text; - -namespace Fido2Identity; - -public class Fido2Store -{ - private readonly ApplicationDbContext _applicationDbContext; - - public Fido2Store(ApplicationDbContext applicationDbContext) - { - _applicationDbContext = applicationDbContext; - } - - public async Task> GetCredentialsByUserNameAsync(string username) - { - return await _applicationDbContext.FidoStoredCredential.Where(c => c.UserName == username).ToListAsync(); - } - - public async Task RemoveCredentialsByUserNameAsync(string username) - { - var items = await _applicationDbContext.FidoStoredCredential.Where(c => c.UserName == username).ToListAsync(); - if (items != null) - { - foreach (var fido2Key in items) - { - _applicationDbContext.FidoStoredCredential.Remove(fido2Key); - }; - - await _applicationDbContext.SaveChangesAsync(); - } - } - - public async Task GetCredentialByIdAsync(byte[] id) - { - var credentialIdString = Base64Url.Encode(id); - //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); - - var cred = await _applicationDbContext.FidoStoredCredential - .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)) - .FirstOrDefaultAsync(); - - return cred; - } - - public Task> GetCredentialsByUserHandleAsync(byte[] userHandle) - { - return Task.FromResult>( - _applicationDbContext - .FidoStoredCredential.Where(c => c.UserHandle != null && c.UserHandle.SequenceEqual(userHandle)) - .ToList()); - } - - public async Task UpdateCounterAsync(byte[] credentialId, uint counter) - { - var credentialIdString = Base64Url.Encode(credentialId); - //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); - - var cred = await _applicationDbContext.FidoStoredCredential - .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync(); - - if (cred != null) - { - cred.SignatureCounter = counter; - await _applicationDbContext.SaveChangesAsync(); - } - } - - public async Task AddCredentialToUserAsync(Fido2User user, FidoStoredCredential credential) - { - credential.UserId = user.Id; - _applicationDbContext.FidoStoredCredential.Add(credential); - await _applicationDbContext.SaveChangesAsync(); - } - - public async Task> GetUsersByCredentialIdAsync(byte[] credentialId) - { - var credentialIdString = Base64Url.Encode(credentialId); - //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); - - var cred = await _applicationDbContext.FidoStoredCredential - .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync(); - - if (cred == null || cred.UserId == null) - { - return new List(); - } - - return await _applicationDbContext.Users - .Where(u => Encoding.UTF8.GetBytes(u.UserName) - .SequenceEqual(cred.UserId)) - .Select(u => new Fido2User - { - DisplayName = u.UserName, - Name = u.UserName, - Id = Encoding.UTF8.GetBytes(u.UserName) // byte representation of userID is required - }).ToListAsync(); - } -} - -public static class Fido2Extenstions -{ - public static IEnumerable NotNull(this IEnumerable enumerable) where T : class - { - return enumerable.Where(e => e != null).Select(e => e!); - } -} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/Fido2UserTwoFactorTokenProvider.cs b/MultiIdentityProvider/IdentityProvider/Fido2/Fido2UserTwoFactorTokenProvider.cs deleted file mode 100644 index 121478f..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/Fido2UserTwoFactorTokenProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using OpeniddictServer.Data; - -namespace Fido2Identity; - -public class Fido2UserTwoFactorTokenProvider : IUserTwoFactorTokenProvider -{ - public Task CanGenerateTwoFactorTokenAsync(UserManager manager, ApplicationUser user) - { - return Task.FromResult(true); - } - - public Task GenerateAsync(string purpose, UserManager manager, ApplicationUser user) - { - return Task.FromResult("fido2"); - } - - public Task ValidateAsync(string purpose, string token, UserManager manager, ApplicationUser user) - { - return Task.FromResult(true); - } -} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/FidoStoredCredential.cs b/MultiIdentityProvider/IdentityProvider/Fido2/FidoStoredCredential.cs deleted file mode 100644 index ef69742..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/FidoStoredCredential.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Fido2NetLib.Objects; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json; - -namespace Fido2Identity; - -/// -/// Represents a WebAuthn credential. -/// -public class FidoStoredCredential -{ - /// - /// Gets or sets the primary key for this user. - /// - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public virtual int Id { get; set; } - - /// - /// Gets or sets the user name for this user. - /// - public virtual string? UserName { get; set; } - - public virtual byte[]? UserId { get; set; } - - /// - /// Gets or sets the public key for this user. - /// - public virtual byte[]? PublicKey { get; set; } - - /// - /// Gets or sets the user handle for this user. - /// - public virtual byte[]? UserHandle { get; set; } - - public virtual uint SignatureCounter { get; set; } - - public virtual string? CredType { get; set; } - - /// - /// Gets or sets the registration date for this user. - /// - public virtual DateTime RegDate { get; set; } - - /// - /// Gets or sets the Authenticator Attestation GUID (AAGUID) for this user. - /// - /// - /// An AAGUID is a 128-bit identifier indicating the type of the authenticator. - /// - public virtual Guid AaGuid { get; set; } - - [NotMapped] - public PublicKeyCredentialDescriptor? Descriptor - { - get { return string.IsNullOrWhiteSpace(DescriptorJson) ? null : JsonSerializer.Deserialize(DescriptorJson); } - set { DescriptorJson = JsonSerializer.Serialize(value); } - } - - public virtual string? DescriptorJson { get; set; } -} diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2RegisterController.cs b/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2RegisterController.cs deleted file mode 100644 index 9e26cdb..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2RegisterController.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Fido2NetLib; -using Fido2NetLib.Objects; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using OpeniddictServer.Data; -using System.Text; -using static Fido2NetLib.Fido2; - -namespace Fido2Identity; - -[Route("api/[controller]")] -public class MfaFido2RegisterController : Controller -{ - private readonly Fido2 _lib; - public static IMetadataService? _mds; - private readonly Fido2Store _fido2Store; - private readonly UserManager _userManager; - private readonly IOptions _optionsFido2Configuration; - - public MfaFido2RegisterController( - Fido2Store fido2Store, - UserManager userManager, - IOptions optionsFido2Configuration) - { - _userManager = userManager; - _optionsFido2Configuration = optionsFido2Configuration; - _fido2Store = fido2Store; - - _lib = new Fido2(new Fido2Configuration() - { - ServerDomain = _optionsFido2Configuration.Value.ServerDomain, - ServerName = _optionsFido2Configuration.Value.ServerName, - Origins = _optionsFido2Configuration.Value.Origins, - TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance - }); - } - - private static string FormatException(Exception e) - { - return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/mfamakeCredentialOptions")] - public async Task MakeCredentialOptions([FromForm] string username, [FromForm] string displayName, [FromForm] string attType, [FromForm] string authType, [FromForm] bool requireResidentKey, [FromForm] string userVerification) - { - try - { - if (string.IsNullOrEmpty(username)) - { - username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})"; - } - - var ApplicationUser = await _userManager.FindByEmailAsync(username); - var user = new Fido2User - { - DisplayName = ApplicationUser.UserName, - Name = ApplicationUser.UserName, - Id = Encoding.UTF8.GetBytes(ApplicationUser.UserName) // byte representation of userID is required - }; - - // 2. Get user existing keys by username - var items = await _fido2Store.GetCredentialsByUserNameAsync(ApplicationUser.UserName); - var existingKeys = new List(); - foreach (var publicKeyCredentialDescriptor in items) - { - if (publicKeyCredentialDescriptor.Descriptor != null) - existingKeys.Add(publicKeyCredentialDescriptor.Descriptor); - } - - // 3. Create options - var authenticatorSelection = new AuthenticatorSelection - { - RequireResidentKey = requireResidentKey, - UserVerification = userVerification.ToEnum() - }; - - if (!string.IsNullOrEmpty(authType)) - authenticatorSelection.AuthenticatorAttachment = authType.ToEnum(); - - var exts = new AuthenticationExtensionsClientInputs - { - Extensions = true, - UserVerificationMethod = true, - }; - - var options = _lib.RequestNewCredential( - user, existingKeys, - authenticatorSelection, attType.ToEnum(), exts); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); - - // 5. return options to client - return Json(options); - } - catch (Exception e) - { - return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) }); - } - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/mfamakeCredential")] - public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse) - { - try - { - // 1. get the options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); - var options = CredentialCreateOptions.FromJson(jsonOptions); - - // 2. Create callback so that lib can verify credential id is unique to this user - async Task callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken) - { - var users = await _fido2Store.GetUsersByCredentialIdAsync(args.CredentialId); - if (users.Count > 0) return false; - - return true; - } - - // 2. Verify and make the credentials - var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback); - - if (success.Result != null) - { - // 3. Store the credentials in db - await _fido2Store.AddCredentialToUserAsync(options.User, new FidoStoredCredential - { - UserName = options.User.Name, - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), - PublicKey = success.Result.PublicKey, - UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter, - CredType = success.Result.CredType, - RegDate = DateTime.Now, - AaGuid = success.Result.Aaguid - }); - } - - // 4. return "ok" to the client - - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return Json(new CredentialMakeResult("error", - $"Unable to load user with ID '{_userManager.GetUserId(User)}'.", - success.Result)); - } - - await _userManager.SetTwoFactorEnabledAsync(user, true); - var userId = await _userManager.GetUserIdAsync(user); - - return Json(success); - } - catch (Exception e) - { - return Json(new CredentialMakeResult("error", FormatException(e), null)); - } - } -} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2SignInFidoController.cs b/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2SignInFidoController.cs deleted file mode 100644 index a2e7090..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/MfaFido2SignInFidoController.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Fido2NetLib; -using Fido2NetLib.Objects; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using OpeniddictServer.Data; -using System.Text; - -namespace Fido2Identity; - -[Route("api/[controller]")] -public class MfaFido2SignInFidoController : Controller -{ - private readonly Fido2 _lib; - private readonly Fido2Store _fido2Store; - private readonly SignInManager _signInManager; - private readonly IOptions _optionsFido2Configuration; - - public MfaFido2SignInFidoController( - Fido2Store fido2Store, - SignInManager signInManager, - IOptions optionsFido2Configuration) - { - _optionsFido2Configuration = optionsFido2Configuration; - _signInManager = signInManager; - _fido2Store = fido2Store; - - _lib = new Fido2(new Fido2Configuration() - { - ServerDomain = _optionsFido2Configuration.Value.ServerDomain, - ServerName = _optionsFido2Configuration.Value.ServerName, - Origins = _optionsFido2Configuration.Value.Origins, - TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance - }); - } - - private static string FormatException(Exception e) - { - return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/mfaassertionOptions")] - public async Task AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification) - { - try - { - var ApplicationUser = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (ApplicationUser == null) - { - throw new InvalidOperationException($"Unable to load two-factor authentication user."); - } - - var existingCredentials = new List(); - - if (!string.IsNullOrEmpty(ApplicationUser.UserName)) - { - - var user = new Fido2User - { - DisplayName = ApplicationUser.UserName, - Name = ApplicationUser.UserName, - Id = Encoding.UTF8.GetBytes(ApplicationUser.UserName) // byte representation of userID is required - }; - - if (user == null) throw new ArgumentException("Username was not registered"); - - // 2. Get registered credentials from database - var items = await _fido2Store.GetCredentialsByUserNameAsync(ApplicationUser.UserName); - existingCredentials = items.Select(c => c.Descriptor).NotNull().ToList(); - } - - var exts = new AuthenticationExtensionsClientInputs - { - UserVerificationMethod = true, - }; - // 3. Create options - var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum(); - var options = _lib.GetAssertionOptions( - existingCredentials, - uv, - exts - ); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); - - // 5. Return options to client - return Json(options); - } - - catch (Exception e) - { - return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) }); - } - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/mfamakeAssertion")] - public async Task MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse) - { - try - { - // 1. Get the assertion options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); - var options = AssertionOptions.FromJson(jsonOptions); - - // 2. Get registered credential from database - var creds = await _fido2Store.GetCredentialByIdAsync(clientResponse.Id); - - if (creds == null) - { - throw new Exception("Unknown credentials"); - } - - // 3. Get credential counter from database - var storedCounter = creds.SignatureCounter; - - // 4. Create callback to check if userhandle owns the credentialId - IsUserHandleOwnerOfCredentialIdAsync callback = async (args, cancellationToken) => - { - var storedCreds = await _fido2Store.GetCredentialsByUserHandleAsync(args.UserHandle); - return storedCreds.Any(c => c.Descriptor != null && c.Descriptor.Id.SequenceEqual(args.CredentialId)); - }; - - if (creds.PublicKey == null) - { - throw new InvalidOperationException($"No public key"); - } - - // 5. Make the assertion - var res = await _lib.MakeAssertionAsync( - clientResponse, options, creds.PublicKey, storedCounter, callback); - - // 6. Store the updated counter - await _fido2Store.UpdateCounterAsync(res.CredentialId, res.Counter); - - // complete sign-in - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - throw new InvalidOperationException($"Unable to load two-factor authentication user."); - } - - var result = await _signInManager.TwoFactorSignInAsync("FIDO2", string.Empty, false, false); - - // 7. return OK to client - return Json(res); - } - catch (Exception e) - { - return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) }); - } - } -} diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2RegisterController.cs b/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2RegisterController.cs deleted file mode 100644 index d860548..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2RegisterController.cs +++ /dev/null @@ -1,175 +0,0 @@ -using Fido2NetLib; -using Fido2NetLib.Objects; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using OpeniddictServer.Data; -using System.Text; -using static Fido2NetLib.Fido2; - -namespace Fido2Identity; - -[Route("api/[controller]")] -public class PwFido2RegisterController : Controller -{ - private readonly Fido2 _lib; - private readonly Fido2Store _fido2Store; - private readonly UserManager _userManager; - private readonly IOptions _optionsFido2Configuration; - - - public PwFido2RegisterController( - Fido2Store fido2Store, - UserManager userManager, - IOptions optionsFido2Configuration) - { - _userManager = userManager; - _optionsFido2Configuration = optionsFido2Configuration; - _fido2Store = fido2Store; - - _lib = new Fido2(new Fido2Configuration() - { - ServerDomain = _optionsFido2Configuration.Value.ServerDomain, - ServerName = _optionsFido2Configuration.Value.ServerName, - Origins = _optionsFido2Configuration.Value.Origins, - TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance - }); - } - - private static string FormatException(Exception e) - { - return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/pwmakeCredentialOptions")] - public async Task MakeCredentialOptions([FromForm] string username, [FromForm] string displayName, [FromForm] string attType, [FromForm] string authType, [FromForm] bool requireResidentKey, [FromForm] string userVerification) - { - try - { - if (string.IsNullOrEmpty(username)) - { - username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})"; - } - - var user = new Fido2User - { - DisplayName = displayName, - Name = username, - Id = Encoding.UTF8.GetBytes(username) // byte representation of userID is required - }; - - // 2. Get user existing keys by username - var items = await _fido2Store.GetCredentialsByUserNameAsync(username); - var existingKeys = new List(); - foreach (var publicKeyCredentialDescriptor in items) - { - if (publicKeyCredentialDescriptor.Descriptor != null) - existingKeys.Add(publicKeyCredentialDescriptor.Descriptor); - } - - // 3. Create options - var authenticatorSelection = new AuthenticatorSelection - { - RequireResidentKey = requireResidentKey, - UserVerification = userVerification.ToEnum() - }; - - if (!string.IsNullOrEmpty(authType)) - authenticatorSelection.AuthenticatorAttachment = authType.ToEnum(); - - var exts = new AuthenticationExtensionsClientInputs - { - Extensions = true, - UserVerificationMethod = true, - }; - - var options = _lib.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); - - // 5. return options to client - return Json(options); - } - catch (Exception e) - { - return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) }); - } - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/pwmakeCredential")] - public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse) - { - try - { - // 1. get the options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); - var options = CredentialCreateOptions.FromJson(jsonOptions); - - // 2. Create callback so that lib can verify credential id is unique to this user - IsCredentialIdUniqueToUserAsyncDelegate callback = async (args, cancellationToken) => - { - var users = await _fido2Store.GetUsersByCredentialIdAsync(args.CredentialId); - if (users.Count > 0) return false; - - return true; - }; - - // 2. Verify and make the credentials - var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback); - - if (success.Result != null) - { - // 3. Store the credentials in db - await _fido2Store.AddCredentialToUserAsync(options.User, new FidoStoredCredential - { - UserName = options.User.Name, - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), - PublicKey = success.Result.PublicKey, - UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter, - CredType = success.Result.CredType, - RegDate = DateTime.Now, - AaGuid = success.Result.Aaguid - }); - } - - // 4. return "ok" to the client - - var user = await CreateUser(options.User.Name); - // await _userManager.GetUserAsync(User); - - if (user == null) - { - return Json(new CredentialMakeResult("error", - $"Unable to load user with ID '{_userManager.GetUserId(User)}'.", - success.Result)); - } - - //await _userManager.SetTwoFactorEnabledAsync(user, true); - //var userId = await _userManager.FindByNameAsync(user); - - return Json(success); - } - catch (Exception e) - { - return Json(new CredentialMakeResult("error", FormatException(e), null)); - } - } - - private async Task CreateUser(string userEmail) - { - var user = new ApplicationUser { UserName = userEmail, Email = userEmail, EmailConfirmed = true }; - var result = await _userManager.CreateAsync(user); - if (result.Succeeded) - { - //await _signInManager.SignInAsync(user, isPersistent: false); - } - - return user; - } -} \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2SignInController.cs b/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2SignInController.cs deleted file mode 100644 index c2f8def..0000000 --- a/MultiIdentityProvider/IdentityProvider/Fido2/PwFido2SignInController.cs +++ /dev/null @@ -1,156 +0,0 @@ -using Fido2NetLib; -using Fido2NetLib.Objects; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using OpeniddictServer.Data; -using System.Text; - -namespace Fido2Identity; - -[Route("api/[controller]")] -public class PwFido2SignInController : Controller -{ - private readonly Fido2 _lib; - private readonly Fido2Store _fido2Store; - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly IOptions _optionsFido2Configuration; - - public PwFido2SignInController( - Fido2Store fido2Store, - UserManager userManager, - SignInManager signInManager, - IOptions optionsFido2Configuration) - { - _userManager = userManager; - _optionsFido2Configuration = optionsFido2Configuration; - _signInManager = signInManager; - _userManager = userManager; - _fido2Store = fido2Store; - - _lib = new Fido2(new Fido2Configuration() - { - ServerDomain = _optionsFido2Configuration.Value.ServerDomain, - ServerName = _optionsFido2Configuration.Value.ServerName, - Origins = _optionsFido2Configuration.Value.Origins, - TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance - }); - } - - private static string FormatException(Exception e) - { - return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/pwassertionOptions")] - public async Task AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification) - { - try - { - - var existingCredentials = new List(); - - if (!string.IsNullOrEmpty(username)) - { - var ApplicationUser = await _userManager.FindByNameAsync(username); - var user = new Fido2User - { - DisplayName = ApplicationUser.UserName, - Name = ApplicationUser.UserName, - Id = Encoding.UTF8.GetBytes(ApplicationUser.UserName) // byte representation of userID is required - }; - - if (user == null) throw new ArgumentException("Username was not registered"); - - // 2. Get registered credentials from database - var items = await _fido2Store.GetCredentialsByUserNameAsync(ApplicationUser.UserName); - existingCredentials = items.Select(c => c.Descriptor).NotNull().ToList(); - } - - var exts = new AuthenticationExtensionsClientInputs - { - UserVerificationMethod = true, - }; - - // 3. Create options - var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum(); - var options = _lib.GetAssertionOptions( - existingCredentials, - uv, - exts - ); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); - - // 5. Return options to client - return Json(options); - } - - catch (Exception e) - { - return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) }); - } - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("/pwmakeAssertion")] - public async Task MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse) - { - try - { - // 1. Get the assertion options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); - var options = AssertionOptions.FromJson(jsonOptions); - - // 2. Get registered credential from database - var creds = await _fido2Store.GetCredentialByIdAsync(clientResponse.Id); - - if (creds == null) - { - throw new Exception("Unknown credentials"); - } - - // 3. Get credential counter from database - var storedCounter = creds.SignatureCounter; - - // 4. Create callback to check if userhandle owns the credentialId - IsUserHandleOwnerOfCredentialIdAsync callback = async (args, cancellationToken) => - { - var storedCreds = await _fido2Store.GetCredentialsByUserHandleAsync(args.UserHandle); - return storedCreds.Any(c => c.Descriptor != null && c.Descriptor.Id.SequenceEqual(args.CredentialId)); - }; - - if (creds.PublicKey == null) - { - throw new InvalidOperationException($"No public key"); - } - - // 5. Make the assertion - var res = await _lib.MakeAssertionAsync( - clientResponse, options, creds.PublicKey, storedCounter, callback); - - // 6. Store the updated counter - await _fido2Store.UpdateCounterAsync(res.CredentialId, res.Counter); - - var ApplicationUser = await _userManager.FindByNameAsync(creds.UserName); - if (ApplicationUser == null) - { - throw new InvalidOperationException($"Unable to load user."); - } - - await _signInManager.SignInAsync(ApplicationUser, isPersistent: false); - - // 7. return OK to client - return Json(res); - } - catch (Exception e) - { - return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) }); - } - } -} diff --git a/MultiIdentityProvider/IdentityProvider/Helpers/AsyncEnumerableExtensions.cs b/MultiIdentityProvider/IdentityProvider/Helpers/AsyncEnumerableExtensions.cs index ccf4aed..56eef10 100644 --- a/MultiIdentityProvider/IdentityProvider/Helpers/AsyncEnumerableExtensions.cs +++ b/MultiIdentityProvider/IdentityProvider/Helpers/AsyncEnumerableExtensions.cs @@ -1,4 +1,4 @@ -namespace OpeniddictServer.Helpers; +namespace IdentityProvider.Helpers; public static class AsyncEnumerableExtensions { diff --git a/MultiIdentityProvider/IdentityProvider/Helpers/FormValueRequiredAttribute.cs b/MultiIdentityProvider/IdentityProvider/Helpers/FormValueRequiredAttribute.cs index 7989c18..8fe5628 100644 --- a/MultiIdentityProvider/IdentityProvider/Helpers/FormValueRequiredAttribute.cs +++ b/MultiIdentityProvider/IdentityProvider/Helpers/FormValueRequiredAttribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; -namespace OpeniddictServer.Helpers; +namespace IdentityProvider.Helpers; public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute { diff --git a/MultiIdentityProvider/IdentityProvider/HostingExtensions.cs b/MultiIdentityProvider/IdentityProvider/HostingExtensions.cs index 7ef777b..711655b 100644 --- a/MultiIdentityProvider/IdentityProvider/HostingExtensions.cs +++ b/MultiIdentityProvider/IdentityProvider/HostingExtensions.cs @@ -1,15 +1,14 @@ -using Fido2Identity; -using Fido2NetLib; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; -using OpeniddictServer.Data; +using IdentityProvider.Data; +using IdentityProvider.Passkeys; using Quartz; using Serilog; using static OpenIddict.Abstractions.OpenIddictConstants; +using Microsoft.IdentityModel.JsonWebTokens; -namespace OpeniddictServer; +namespace IdentityProvider; internal static class HostingExtensions { @@ -21,9 +20,11 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde services.AddControllersWithViews(); services.AddRazorPages(); + services.AddHttpContextAccessor(); + services.AddDbContext(options => { - // Configure the context to use Microsoft SQL Server. + // Configure the context to use Microsoft SQLite. options.UseSqlite(configuration.GetConnectionString("DefaultConnection")); // Register the entity sets needed by OpenIddict. @@ -34,14 +35,13 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde services.AddDatabaseDeveloperPageExceptionFilter(); - services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders() - .AddDefaultUI() - .AddTokenProvider("FIDO2"); - - services.Configure(configuration.GetSection("fido2")); - services.AddScoped(); + services.AddIdentity(options => + { + options.Stores.SchemaVersion = IdentitySchemaVersions.Version3; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders() + .AddDefaultUI(); services.AddDistributedMemoryCache(); @@ -79,21 +79,6 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde options.UseInMemoryStore(); }); - services.AddCors(options => - { - options.AddPolicy("AllowAllOrigins", - builder => - { - builder - .AllowCredentials() - .WithOrigins( - "https://localhost:4200") - .SetIsOriginAllowedToAllowWildcardSubdomains() - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - // Register the Quartz.NET service and configure it to block shutdown until jobs are complete. services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); @@ -162,7 +147,6 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde // Note: in a real world application, this step should be part of a setup script. services.AddHostedService(); - return builder.Build(); } @@ -185,21 +169,23 @@ public static WebApplication ConfigurePipeline(this WebApplication app) //app.UseHsts(); } - app.UseCors("AllowAllOrigins"); - app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); + app.MapStaticAssets(); + app.UseAntiforgery(); app.UseSession(); + app.MapPasskeyEndpoints(); + app.MapControllers(); app.MapDefaultControllerRoute(); - app.MapRazorPages(); + app.MapRazorPages() + .WithStaticAssets(); return app; } diff --git a/MultiIdentityProvider/IdentityProvider/IdentityProvider.csproj b/MultiIdentityProvider/IdentityProvider/IdentityProvider.csproj index d89eca2..56958b8 100644 --- a/MultiIdentityProvider/IdentityProvider/IdentityProvider.csproj +++ b/MultiIdentityProvider/IdentityProvider/IdentityProvider.csproj @@ -9,21 +9,24 @@ - - - - - - - - + + + + + + + + + + - + + + - diff --git a/MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.Designer.cs b/MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.Designer.cs deleted file mode 100644 index cbe48e3..0000000 --- a/MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.Designer.cs +++ /dev/null @@ -1,539 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpeniddictServer.Data; - -#nullable disable - -namespace IdentityProvider.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20220827060047_add-fido2")] - partial class addfido2 - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); - - modelBuilder.Entity("Fido2Identity.FidoStoredCredential", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AaGuid") - .HasColumnType("TEXT"); - - b.Property("CredType") - .HasColumnType("TEXT"); - - b.Property("DescriptorJson") - .HasColumnType("TEXT"); - - b.Property("PublicKey") - .HasColumnType("BLOB"); - - b.Property("RegDate") - .HasColumnType("TEXT"); - - b.Property("SignatureCounter") - .HasColumnType("INTEGER"); - - b.Property("UserHandle") - .HasColumnType("BLOB"); - - b.Property("UserId") - .HasColumnType("BLOB"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("FidoStoredCredential"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ClientId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ClientSecret") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ConsentType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("DisplayNames") - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("TEXT"); - - b.Property("PostLogoutRedirectUris") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("RedirectUris") - .HasColumnType("TEXT"); - - b.Property("Requirements") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("OpenIddictApplications", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Scopes") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("TEXT"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictAuthorizations", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("Descriptions") - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("DisplayNames") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Resources") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("OpenIddictScopes", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("AuthorizationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("ExpirationDate") - .HasColumnType("TEXT"); - - b.Property("Payload") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("RedemptionDate") - .HasColumnType("TEXT"); - - b.Property("ReferenceId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("TEXT"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorizationId"); - - b.HasIndex("ReferenceId") - .IsUnique(); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictTokens", (string)null); - }); - - modelBuilder.Entity("OpeniddictServer.Data.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Authorizations") - .HasForeignKey("ApplicationId"); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Tokens") - .HasForeignKey("ApplicationId"); - - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") - .WithMany("Tokens") - .HasForeignKey("AuthorizationId"); - - b.Navigation("Application"); - - b.Navigation("Authorization"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Navigation("Authorizations"); - - b.Navigation("Tokens"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Navigation("Tokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.cs b/MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.cs deleted file mode 100644 index 22ed3ef..0000000 --- a/MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace IdentityProvider.Migrations -{ - /// - public partial class oidcupdate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "Type", - table: "OpenIddictApplications", - newName: "ClientType"); - - migrationBuilder.AddColumn( - name: "ApplicationType", - table: "OpenIddictApplications", - type: "TEXT", - maxLength: 50, - nullable: true); - - migrationBuilder.AddColumn( - name: "JsonWebKeySet", - table: "OpenIddictApplications", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "Settings", - table: "OpenIddictApplications", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ApplicationType", - table: "OpenIddictApplications"); - - migrationBuilder.DropColumn( - name: "JsonWebKeySet", - table: "OpenIddictApplications"); - - migrationBuilder.DropColumn( - name: "Settings", - table: "OpenIddictApplications"); - - migrationBuilder.RenameColumn( - name: "ClientType", - table: "OpenIddictApplications", - newName: "Type"); - } - } -} diff --git a/MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.Designer.cs b/MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.Designer.cs similarity index 85% rename from MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.Designer.cs rename to MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.Designer.cs index 289a4a5..b5eae2d 100644 --- a/MultiIdentityProvider/IdentityProvider/Migrations/20240404095401_oidc-update.Designer.cs +++ b/MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.Designer.cs @@ -1,550 +1,585 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpeniddictServer.Data; - -#nullable disable - -namespace IdentityProvider.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20240404095401_oidc-update")] - partial class oidcupdate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); - - modelBuilder.Entity("Fido2Identity.FidoStoredCredential", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AaGuid") - .HasColumnType("TEXT"); - - b.Property("CredType") - .HasColumnType("TEXT"); - - b.Property("DescriptorJson") - .HasColumnType("TEXT"); - - b.Property("PublicKey") - .HasColumnType("BLOB"); - - b.Property("RegDate") - .HasColumnType("TEXT"); - - b.Property("SignatureCounter") - .HasColumnType("INTEGER"); - - b.Property("UserHandle") - .HasColumnType("BLOB"); - - b.Property("UserId") - .HasColumnType("BLOB"); - - b.Property("UserName") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("FidoStoredCredential"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ClientId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ClientSecret") - .HasColumnType("TEXT"); - - b.Property("ClientType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ConsentType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("DisplayNames") - .HasColumnType("TEXT"); - - b.Property("JsonWebKeySet") - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("TEXT"); - - b.Property("PostLogoutRedirectUris") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("RedirectUris") - .HasColumnType("TEXT"); - - b.Property("Requirements") - .HasColumnType("TEXT"); - - b.Property("Settings") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("OpenIddictApplications", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Scopes") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("TEXT"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictAuthorizations", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("Descriptions") - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("DisplayNames") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Resources") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("OpenIddictScopes", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("AuthorizationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("ExpirationDate") - .HasColumnType("TEXT"); - - b.Property("Payload") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("RedemptionDate") - .HasColumnType("TEXT"); - - b.Property("ReferenceId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("TEXT"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorizationId"); - - b.HasIndex("ReferenceId") - .IsUnique(); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictTokens", (string)null); - }); - - modelBuilder.Entity("OpeniddictServer.Data.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Authorizations") - .HasForeignKey("ApplicationId"); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Tokens") - .HasForeignKey("ApplicationId"); - - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") - .WithMany("Tokens") - .HasForeignKey("AuthorizationId"); - - b.Navigation("Application"); - - b.Navigation("Authorization"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Navigation("Authorizations"); - - b.Navigation("Tokens"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Navigation("Tokens"); - }); -#pragma warning restore 612, 618 - } - } -} +// +using System; +using IdentityProvider.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IdentityProvider.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260224203342_InitSts")] + partial class InitSts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + + modelBuilder.Entity("IdentityProvider.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("BLOB"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserPasskeys", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("JsonWebKeySet") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedirectUris") + .HasColumnType("TEXT"); + + b.Property("Requirements") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Scopes") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Descriptions") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Resources") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("AuthorizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Payload") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedemptionDate") + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject") + .IsRequired(); + + b1.Property("ClientDataJson") + .IsRequired(); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey") + .IsRequired(); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AspNetUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("TEXT"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data") + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.cs b/MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.cs similarity index 90% rename from MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.cs rename to MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.cs index 9fe228d..1a1926f 100644 --- a/MultiIdentityProvider/IdentityProvider/Migrations/20220827060047_add-fido2.cs +++ b/MultiIdentityProvider/IdentityProvider/Migrations/20260224203342_InitSts.cs @@ -1,11 +1,14 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace IdentityProvider.Migrations { - public partial class addfido2 : Migration + /// + public partial class InitSts : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( @@ -35,7 +38,7 @@ protected override void Up(MigrationBuilder migrationBuilder) PasswordHash = table.Column(type: "TEXT", nullable: true), SecurityStamp = table.Column(type: "TEXT", nullable: true), ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), - PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", maxLength: 256, nullable: true), PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), LockoutEnd = table.Column(type: "TEXT", nullable: true), @@ -47,44 +50,26 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_AspNetUsers", x => x.Id); }); - migrationBuilder.CreateTable( - name: "FidoStoredCredential", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - UserName = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "BLOB", nullable: true), - PublicKey = table.Column(type: "BLOB", nullable: true), - UserHandle = table.Column(type: "BLOB", nullable: true), - SignatureCounter = table.Column(type: "INTEGER", nullable: false), - CredType = table.Column(type: "TEXT", nullable: true), - RegDate = table.Column(type: "TEXT", nullable: false), - AaGuid = table.Column(type: "TEXT", nullable: false), - DescriptorJson = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_FidoStoredCredential", x => x.Id); - }); - migrationBuilder.CreateTable( name: "OpenIddictApplications", columns: table => new { Id = table.Column(type: "TEXT", nullable: false), + ApplicationType = table.Column(type: "TEXT", maxLength: 50, nullable: true), ClientId = table.Column(type: "TEXT", maxLength: 100, nullable: true), ClientSecret = table.Column(type: "TEXT", nullable: true), + ClientType = table.Column(type: "TEXT", maxLength: 50, nullable: true), ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), ConsentType = table.Column(type: "TEXT", maxLength: 50, nullable: true), DisplayName = table.Column(type: "TEXT", nullable: true), DisplayNames = table.Column(type: "TEXT", nullable: true), + JsonWebKeySet = table.Column(type: "TEXT", nullable: true), Permissions = table.Column(type: "TEXT", nullable: true), PostLogoutRedirectUris = table.Column(type: "TEXT", nullable: true), Properties = table.Column(type: "TEXT", nullable: true), RedirectUris = table.Column(type: "TEXT", nullable: true), Requirements = table.Column(type: "TEXT", nullable: true), - Type = table.Column(type: "TEXT", maxLength: 50, nullable: true) + Settings = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -156,8 +141,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "AspNetUserLogins", columns: table => new { - LoginProvider = table.Column(type: "TEXT", nullable: false), - ProviderKey = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProviderKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), ProviderDisplayName = table.Column(type: "TEXT", nullable: true), UserId = table.Column(type: "TEXT", nullable: false) }, @@ -172,6 +157,25 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "AspNetUserPasskeys", + columns: table => new + { + CredentialId = table.Column(type: "BLOB", maxLength: 1024, nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserPasskeys", x => x.CredentialId); + table.ForeignKey( + name: "FK_AspNetUserPasskeys_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "AspNetUserRoles", columns: table => new @@ -201,8 +205,8 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { UserId = table.Column(type: "TEXT", nullable: false), - LoginProvider = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), Value = table.Column(type: "TEXT", nullable: true) }, constraints: table => @@ -256,7 +260,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ReferenceId = table.Column(type: "TEXT", maxLength: 100, nullable: true), Status = table.Column(type: "TEXT", maxLength: 50, nullable: true), Subject = table.Column(type: "TEXT", maxLength: 400, nullable: true), - Type = table.Column(type: "TEXT", maxLength: 50, nullable: true) + Type = table.Column(type: "TEXT", maxLength: 150, nullable: true) }, constraints: table => { @@ -294,6 +298,11 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "AspNetUserLogins", column: "UserId"); + migrationBuilder.CreateIndex( + name: "IX_AspNetUserPasskeys_UserId", + table: "AspNetUserPasskeys", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_AspNetUserRoles_RoleId", table: "AspNetUserRoles", @@ -344,6 +353,7 @@ protected override void Up(MigrationBuilder migrationBuilder) unique: true); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( @@ -356,13 +366,13 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "AspNetUserLogins"); migrationBuilder.DropTable( - name: "AspNetUserRoles"); + name: "AspNetUserPasskeys"); migrationBuilder.DropTable( - name: "AspNetUserTokens"); + name: "AspNetUserRoles"); migrationBuilder.DropTable( - name: "FidoStoredCredential"); + name: "AspNetUserTokens"); migrationBuilder.DropTable( name: "OpenIddictScopes"); diff --git a/MultiIdentityProvider/IdentityProvider/Migrations/ApplicationDbContextModelSnapshot.cs b/MultiIdentityProvider/IdentityProvider/Migrations/ApplicationDbContextModelSnapshot.cs index cd68f30..f773f5b 100644 --- a/MultiIdentityProvider/IdentityProvider/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/MultiIdentityProvider/IdentityProvider/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,9 +1,9 @@ // using System; +using IdentityProvider.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpeniddictServer.Data; #nullable disable @@ -15,44 +15,71 @@ partial class ApplicationDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); - modelBuilder.Entity("Fido2Identity.FidoStoredCredential", b => + modelBuilder.Entity("IdentityProvider.Data.ApplicationUser", b => { - b.Property("Id") - .ValueGeneratedOnAdd() + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") .HasColumnType("INTEGER"); - b.Property("AaGuid") + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() .HasColumnType("TEXT"); - b.Property("CredType") + b.Property("Email") + .HasMaxLength(256) .HasColumnType("TEXT"); - b.Property("DescriptorJson") + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") .HasColumnType("TEXT"); - b.Property("PublicKey") - .HasColumnType("BLOB"); + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); - b.Property("RegDate") + b.Property("PhoneNumber") + .HasMaxLength(256) .HasColumnType("TEXT"); - b.Property("SignatureCounter") + b.Property("PhoneNumberConfirmed") .HasColumnType("INTEGER"); - b.Property("UserHandle") - .HasColumnType("BLOB"); + b.Property("SecurityStamp") + .HasColumnType("TEXT"); - b.Property("UserId") - .HasColumnType("BLOB"); + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); b.Property("UserName") + .HasMaxLength(256) .HasColumnType("TEXT"); b.HasKey("Id"); - b.ToTable("FidoStoredCredential"); + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => @@ -130,9 +157,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("ProviderKey") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("ProviderDisplayName") @@ -149,6 +178,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserLogins", (string)null); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("BLOB"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserPasskeys", (string)null); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.Property("UserId") @@ -170,9 +216,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("LoginProvider") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Name") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Value") @@ -376,7 +424,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Type") - .HasMaxLength(50) + .HasMaxLength(150) .HasColumnType("TEXT"); b.HasKey("Id"); @@ -391,70 +439,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OpenIddictTokens", (string)null); }); - modelBuilder.Entity("OpeniddictServer.Data.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -466,7 +450,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) + b.HasOne("IdentityProvider.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -475,11 +459,62 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) + b.HasOne("IdentityProvider.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.HasOne("IdentityProvider.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject") + .IsRequired(); + + b1.Property("ClientDataJson") + .IsRequired(); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey") + .IsRequired(); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AspNetUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("TEXT"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data") + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => @@ -490,7 +525,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) + b.HasOne("IdentityProvider.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -499,7 +534,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("OpeniddictServer.Data.ApplicationUser", null) + b.HasOne("IdentityProvider.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyEndpointRouteBuilderExtensions.cs b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..0ef2f55 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyEndpointRouteBuilderExtensions.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using IdentityProvider.Data; + +namespace IdentityProvider.Passkeys; + +public static class PasskeyEndpointRouteBuilderExtensions +{ + public static IEndpointConventionBuilder MapPasskeyEndpoints(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var accountGroup = endpoints.MapGroup("/Identity/Account").ExcludeFromDescription(); + + accountGroup.MapPost("/PasskeyCreationOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery) => + { + await antiforgery.ValidateRequestAsync(context); + + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + var userId = await userManager.GetUserIdAsync(user); + var userName = await userManager.GetUserNameAsync(user) ?? "User"; + var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new() + { + Id = userId, + Name = userName, + DisplayName = userName + }); + return TypedResults.Content(optionsJson, contentType: "application/json"); + }); + + accountGroup.MapPost("/PasskeyRequestOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery, + [FromQuery] string? username) => + { + await antiforgery.ValidateRequestAsync(context); + + var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username); + var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); + return TypedResults.Content(optionsJson, contentType: "application/json"); + }); + + return accountGroup; + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyOperation.cs b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyOperation.cs new file mode 100644 index 0000000..8a4709f --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeyOperation.cs @@ -0,0 +1,7 @@ +namespace IdentityProvider.Passkeys; + +public enum PasskeyOperation +{ + Create = 0, + Request = 1, +} diff --git a/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeySubmitTagHelper.cs b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeySubmitTagHelper.cs new file mode 100644 index 0000000..b6dcee9 --- /dev/null +++ b/MultiIdentityProvider/IdentityProvider/Passkeys/PasskeySubmitTagHelper.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace IdentityProvider.Passkeys; + +[HtmlTargetElement("passkey-submit")] +public class PasskeySubmitTagHelper : TagHelper +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + [HtmlAttributeName("operation")] + public PasskeyOperation Operation { get; set; } + + [HtmlAttributeName("name")] + public string Name { get; set; } = null!; + + [HtmlAttributeName("email-name")] + public string? EmailName { get; set; } + + public PasskeySubmitTagHelper(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + // Get tokens + var tokens = _httpContextAccessor.HttpContext?.RequestServices + .GetService()?.GetTokens(_httpContextAccessor.HttpContext); + + // Button is the main element we want to create, capture all attributes etc. + var buttonAttributes = output.Attributes.Where(it => it.Name != "operation" && it.Name != "name" && it.Name != "email-name").ToList(); + var buttonContent = (await output.GetChildContentAsync(NullHtmlEncoder.Default)) + .GetContent(NullHtmlEncoder.Default); + + // Create the button + using var htmlWriter = new StringWriter(); + htmlWriter.Write(""); + htmlWriter.WriteLine(); + + // Create the element + htmlWriter.Write(""); + htmlWriter.Write(""); + + // Emit the element + output.TagName = null; + output.Attributes.Clear(); + output.Content.Clear(); + output.Content.SetHtmlContent(htmlWriter.ToString()); + + await base.ProcessAsync(context, output); + } +} diff --git a/MultiIdentityProvider/IdentityProvider/Program.cs b/MultiIdentityProvider/IdentityProvider/Program.cs index 78a8c9e..3f84db2 100644 --- a/MultiIdentityProvider/IdentityProvider/Program.cs +++ b/MultiIdentityProvider/IdentityProvider/Program.cs @@ -1,4 +1,4 @@ -using OpeniddictServer; +using IdentityProvider; using Serilog; Log.Logger = new LoggerConfiguration() diff --git a/MultiIdentityProvider/IdentityProvider/Properties/launchSettings.json b/MultiIdentityProvider/IdentityProvider/Properties/launchSettings.json index a67b468..f872e09 100644 --- a/MultiIdentityProvider/IdentityProvider/Properties/launchSettings.json +++ b/MultiIdentityProvider/IdentityProvider/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "OpeniddictServer": { + "IdentityProvider": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { diff --git a/MultiIdentityProvider/IdentityProvider/ViewModels/Authorization/AuthorizeViewModel.cs b/MultiIdentityProvider/IdentityProvider/ViewModels/Authorization/AuthorizeViewModel.cs index d8091c9..7513ed6 100644 --- a/MultiIdentityProvider/IdentityProvider/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/MultiIdentityProvider/IdentityProvider/ViewModels/Authorization/AuthorizeViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace OpeniddictServer.ViewModels.Authorization; +namespace IdentityProvider.ViewModels.Authorization; public class AuthorizeViewModel { diff --git a/MultiIdentityProvider/IdentityProvider/ViewModels/Shared/ErrorViewModel.cs b/MultiIdentityProvider/IdentityProvider/ViewModels/Shared/ErrorViewModel.cs index 953712d..2fa2df3 100644 --- a/MultiIdentityProvider/IdentityProvider/ViewModels/Shared/ErrorViewModel.cs +++ b/MultiIdentityProvider/IdentityProvider/ViewModels/Shared/ErrorViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace OpeniddictServer.ViewModels.Shared; +namespace IdentityProvider.ViewModels.Shared; public class ErrorViewModel { diff --git a/MultiIdentityProvider/IdentityProvider/Views/Authorization/Authorize.cshtml b/MultiIdentityProvider/IdentityProvider/Views/Authorization/Authorize.cshtml index 549b9a7..5bf21d3 100644 --- a/MultiIdentityProvider/IdentityProvider/Views/Authorization/Authorize.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Views/Authorization/Authorize.cshtml @@ -1,4 +1,9 @@ -@using Microsoft.Extensions.Primitives +@using IdentityProvider +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@using IdentityProvider.ViewModels.Authorization +@using IdentityProvider.ViewModels.Shared +@using Microsoft.Extensions.Primitives @model AuthorizeViewModel
@@ -17,4 +22,4 @@ -
+ \ No newline at end of file diff --git a/MultiIdentityProvider/IdentityProvider/Views/Authorization/Logout.cshtml b/MultiIdentityProvider/IdentityProvider/Views/Authorization/Logout.cshtml index 0f892ec..1a3f1d6 100644 --- a/MultiIdentityProvider/IdentityProvider/Views/Authorization/Logout.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Views/Authorization/Logout.cshtml @@ -1,4 +1,9 @@ -@using Microsoft.Extensions.Primitives +@using IdentityProvider +@using IdentityProvider.Data +@using IdentityProvider.Passkeys +@using IdentityProvider.ViewModels.Authorization +@using IdentityProvider.ViewModels.Shared +@using Microsoft.Extensions.Primitives

Log out

diff --git a/MultiIdentityProvider/IdentityProvider/Views/Shared/_Layout.cshtml b/MultiIdentityProvider/IdentityProvider/Views/Shared/_Layout.cshtml index c67ee64..d6d4dd3 100644 --- a/MultiIdentityProvider/IdentityProvider/Views/Shared/_Layout.cshtml +++ b/MultiIdentityProvider/IdentityProvider/Views/Shared/_Layout.cshtml @@ -3,18 +3,17 @@ - @ViewData["Title"] - IdentityProvider - - + @ViewData["Title"] - OpenIddict Server + + -