diff --git a/.gitignore b/.gitignore index 52ba4abfc..27db03ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -375,3 +375,4 @@ override.tf.json # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan # example: *tfplan* /sample/Aguacongas.TheIdServer.MtlsSample/client.pks +/sample/Aguacongas.TheIdServer.MtlsSample/.dccache diff --git a/src/Aguacongas.TheIdServer.Duende/Localization-fr.json b/src/Aguacongas.TheIdServer.Duende/Localization-fr.json index 0af55533f..833d60a9f 100644 --- a/src/Aguacongas.TheIdServer.Duende/Localization-fr.json +++ b/src/Aguacongas.TheIdServer.Duende/Localization-fr.json @@ -2042,5 +2042,29 @@ { "key": "allowed identity token signing algorithms", "value": "algorithmes de signature de jeton d'identité autorisés" + }, + { + "key": "Reset password", + "value": "Réinitialiser le mot de passe" + }, + { + "key": "Reset your password.", + "value": "Réinitialisez votre mot de passe." + }, + { + "key": "Reset", + "value": "Réinitialiser" + }, + { + "key": "Reset password confirmation", + "value": "Confirmation de la réinitialisation du mot de passe" + }, + { + "key": "Your password has been reset.", + "value": "Votre mot de passe a été réinitialisé." + }, + { + "key": "Please click here to log in", + "value": "Veuillez cliquer ici pour vous connecter" } ] \ No newline at end of file diff --git a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountController.cs b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountController.cs index 0b13d69a6..59e96613b 100644 --- a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountController.cs +++ b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountController.cs @@ -250,6 +250,7 @@ public IActionResult AccessDenied() private async Task BuildLoginViewModelAsync(string returnUrl) { var context = await _interaction.GetAuthorizationContextAsync(returnUrl).ConfigureAwait(false); + if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP).ConfigureAwait(false) != null) { var local = context.IdP == IdentityServerConstants.LocalIdentityProvider; @@ -304,7 +305,10 @@ private async Task BuildLoginViewModelAsync(string returnUrl) EnableLocalLogin = allowLocal && settings.AllowLocalLogin, ReturnUrl = returnUrl, Username = context?.LoginHint, - ExternalProviders = providers.ToArray() + ExternalProviders = providers.ToArray(), + ShowForgotPassworLink = settings.ShowForgotPassworLink, + ShowRegisterLink = settings.ShowRegisterLink, + ShowResendEmailConfirmationLink = settings.ShowResendEmailConfirmationLink }; } diff --git a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountOptions.cs b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountOptions.cs index bc1b93e2c..0ab56a512 100644 --- a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountOptions.cs +++ b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/AccountOptions.cs @@ -20,5 +20,12 @@ public class AccountOptions public bool IncludeWindowsGroups { get; set; } = false; public string InvalidCredentialsErrorMessage { get; set; } = "Invalid username or password"; + + public bool ShowForgotPassworLink { get; set; } = true; + + public bool ShowRegisterLink { get; set; } = true; + + public bool ShowResendEmailConfirmationLink { get; set; } = true; + } } diff --git a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/LoginViewModel.cs b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/LoginViewModel.cs index 506ac9692..e611b8296 100644 --- a/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/LoginViewModel.cs +++ b/src/Aguacongas.TheIdServer.Shared/Quickstart/Account/LoginViewModel.cs @@ -16,5 +16,11 @@ public class LoginViewModel : LoginInputModel public bool IsExternalLoginOnly => !EnableLocalLogin && ExternalProviders?.Count() == 1; public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + + public bool ShowForgotPassworLink { get; set; } = true; + + public bool ShowRegisterLink { get; set; } = true; + + public bool ShowResendEmailConfirmationLink { get; set; } = true; } } \ No newline at end of file diff --git a/src/Aguacongas.TheIdServer.Shared/Views/Account/Login.cshtml b/src/Aguacongas.TheIdServer.Shared/Views/Account/Login.cshtml index 097aebf93..cdc77b185 100644 --- a/src/Aguacongas.TheIdServer.Shared/Views/Account/Login.cshtml +++ b/src/Aguacongas.TheIdServer.Shared/Views/Account/Login.cshtml @@ -41,16 +41,36 @@ - + @if (Model.AllowRememberLogin) { - diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml index f8a2d0aa4..3dfaaac89 100644 --- a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml @@ -16,6 +16,7 @@ Copyright (c) 2022 @Olivier Lefebvre
+
diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index 198df3dad..b5ac3fcdf 100644 --- a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -39,6 +39,15 @@ public class InputModel [Required] [EmailAddress] public string Email { get; set; } + public string ReturnUrl { get; set; } + } + + public void OnGet(string returnUrl = null) + { + Input = new InputModel + { + ReturnUrl = returnUrl + }; } public async Task OnPostAsync() @@ -59,7 +68,7 @@ public async Task OnPostAsync() var callbackUrl = Url.Page( "/Account/ResetPassword", pageHandler: null, - values: new { area = "Identity", code }, + values: new { area = "Identity", code, returnUrl = Input.ReturnUrl }, protocol: Request.Scheme); await _emailSender.SendEmailAsync( diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml new file mode 100644 index 000000000..5ae372e2a --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml @@ -0,0 +1,26 @@ +@page +@model ResendEmailConfirmationModel +@{ + ViewData["Title"] = "Resend email confirmation"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+ +
+
+ + + +
+ + +
+
+ +@section Scripts { + +} diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs new file mode 100644 index 000000000..67d00b899 --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs @@ -0,0 +1,91 @@ +// 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 Aguacongas.TheIdServer.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Localization; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Aguacongas.TheIdServer.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResendEmailConfirmationModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + private readonly IStringLocalizer _localizer; + + public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender, IStringLocalizer localizer) + { + _userManager = userManager; + _emailSender = emailSender; + _localizer = localizer; + } + + /// + /// 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 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. + /// + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + ModelState.AddModelError(string.Empty, _localizer["Verification email sent. Please check your email."]); + return Page(); + } + + var userId = await _userManager.GetUserIdAsync(user); + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = userId, code = code }, + protocol: Request.Scheme); + await _emailSender.SendEmailAsync( + Input.Email, + _localizer["Confirm your email"], + _localizer[$"Please confirm your account by clicking here."]); + + ModelState.AddModelError(string.Empty, _localizer["Verification email sent. Please check your email."]); + return Page(); + } + } +} diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml new file mode 100644 index 000000000..791e1975a --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,44 @@ +@* +Project: Aguafrommars/TheIdServer +Copyright (c) 2022 @Olivier Lefebvre +*@ +@page +@inject IViewLocalizer Localizer + +@model ResetPasswordModel +@{ + ViewData["Title"] = Localizer["Reset password"]; +} + +

@ViewData["Title"]

+

@Localizer["Reset your password."]

+
+
+
+
+
+ + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + +} diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 000000000..3edc286e9 --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,121 @@ +// 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 Aguacongas.TheIdServer.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Threading.Tasks; + +namespace Aguacongas.TheIdServer.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordModel : PageModel + { + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + /// + /// 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 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. + /// + [Required] + [EmailAddress] + [Display(Name = "Email")] + 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] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "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. + /// + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { 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] + public string Code { get; set; } + public string ReturnUrl { get; set; } + } + + public IActionResult OnGet(string code = null, string returnUrl = null) + { + if (code == null) + { + return BadRequest("A code must be supplied for password reset."); + } + else + { + Input = new InputModel + { + Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)), + ReturnUrl = returnUrl + }; + return Page(); + } + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToPage("./ResetPasswordConfirmation", new { returnUrl = Input.ReturnUrl }); + } + + var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + return RedirectToPage("./ResetPasswordConfirmation", new { returnUrl = Input.ReturnUrl }); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + } +} diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 000000000..79c3937d6 --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,15 @@ +@* +Project: Aguafrommars/TheIdServer +Copyright (c) 2022 @Olivier Lefebvre +*@ +@page +@inject IViewLocalizer Localizer +@model ResetPasswordConfirmationModel +@{ + ViewData["Title"] = Localizer["Reset password confirmation"]; +} + +

@ViewData["Title"]

+

+ @Localizer["Your password has been reset."] @Localizer["Please click here to log in"]. +

diff --git a/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 000000000..476ea9213 --- /dev/null +++ b/src/Aguacongas.TheIdServer/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,33 @@ +// 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.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Aguacongas.TheIdServer.Areas.Identity.Pages.Account +{ + /// + /// 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. + /// + [AllowAnonymous] + public class ResetPasswordConfirmationModel : PageModel + { + public string ReturnUrl { get; private 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 void OnGet(string returnUrl = null) + { + ReturnUrl = returnUrl ?? Url.Action(new UrlActionContext + { + Controller = "Account", + Action = "Login" + }); + } + } +}