15 |
16 |
17 |
18 | @code {
19 | private string? message;
20 |
21 | [CascadingParameter]
22 | private HttpContext HttpContext { get; set; } = default!;
23 |
24 | [SupplyParameterFromQuery]
25 | private string? UserId { get; set; }
26 |
27 | [SupplyParameterFromQuery]
28 | private string? Email { get; set; }
29 |
30 | [SupplyParameterFromQuery]
31 | private string? Code { get; set; }
32 |
33 | protected override async Task OnInitializedAsync()
34 | {
35 | if (UserId is null || Email is null || Code is null)
36 | {
37 | RedirectManager.RedirectToWithStatus(
38 | "Account/Login", "Error: Invalid email change confirmation link.", HttpContext);
39 | }
40 |
41 | var user = await UserManager.FindByIdAsync(UserId);
42 | if (user is null)
43 | {
44 | message = "Unable to find user with Id '{userId}'";
45 | return;
46 | }
47 |
48 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
49 | var result = await UserManager.ChangeEmailAsync(user, Email, code);
50 | if (!result.Succeeded)
51 | {
52 | message = "Error changing email.";
53 | return;
54 | }
55 |
56 | // In our UI email and user name are one and the same, so when we update the email
57 | // we need to update the user name.
58 | var setUserNameResult = await UserManager.SetUserNameAsync(user, Email);
59 | if (!setUserNameResult.Succeeded)
60 | {
61 | message = "Error changing user name.";
62 | return;
63 | }
64 |
65 | await SignInManager.RefreshSignInAsync(user);
66 | message = "Thank you for confirming your email change.";
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ExternalLogin.razor:
--------------------------------------------------------------------------------
1 | @page "/Account/ExternalLogin"
2 |
3 | @using System.ComponentModel.DataAnnotations
4 | @using System.Security.Claims
5 | @using System.Text
6 | @using System.Text.Encodings.Web
7 | @using Microsoft.AspNetCore.Identity
8 | @using Microsoft.AspNetCore.WebUtilities
9 | @using CleanBlazorWeb.Data
10 |
11 | @inject SignInManager SignInManager
12 | @inject UserManager UserManager
13 | @inject IUserStore UserStore
14 | @inject IEmailSender EmailSender
15 | @inject NavigationManager NavigationManager
16 | @inject IdentityRedirectManager RedirectManager
17 | @inject ILogger Logger
18 |
19 | Register
20 |
21 |
22 |
Register
23 |
Associate your @ProviderDisplayName account.
24 |
25 |
26 |
27 | You've successfully authenticated with @ProviderDisplayName.
28 | Please enter an email address for this site below and click the Register button to finish
29 | logging in.
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | @code {
48 | public const string LoginCallbackAction = "LoginCallback";
49 |
50 | private string? message;
51 | private ExternalLoginInfo externalLoginInfo = default!;
52 |
53 | [CascadingParameter]
54 | private HttpContext HttpContext { get; set; } = default!;
55 |
56 | [SupplyParameterFromForm]
57 | private InputModel Input { get; set; } = new();
58 |
59 | [SupplyParameterFromQuery]
60 | private string? RemoteError { get; set; }
61 |
62 | [SupplyParameterFromQuery]
63 | private string? ReturnUrl { get; set; }
64 |
65 | [SupplyParameterFromQuery]
66 | private string? Action { get; set; }
67 |
68 | private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName;
69 |
70 | protected override async Task OnInitializedAsync()
71 | {
72 | if (RemoteError is not null)
73 | {
74 | RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
75 | }
76 |
77 | var info = await SignInManager.GetExternalLoginInfoAsync();
78 | if (info is null)
79 | {
80 | RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
81 | }
82 |
83 | externalLoginInfo = info;
84 |
85 | if (HttpMethods.IsGet(HttpContext.Request.Method))
86 | {
87 | if (Action == LoginCallbackAction)
88 | {
89 | await OnLoginCallbackAsync();
90 | return;
91 | }
92 |
93 | // We should only reach this page via the login callback, so redirect back to
94 | // the login page if we get here some other way.
95 | RedirectManager.RedirectTo("Account/Login");
96 | }
97 | }
98 |
99 | private async Task OnLoginCallbackAsync()
100 | {
101 | // Sign in the user with this external login provider if the user already has a login.
102 | var result = await SignInManager.ExternalLoginSignInAsync(
103 | externalLoginInfo.LoginProvider,
104 | externalLoginInfo.ProviderKey,
105 | isPersistent: false,
106 | bypassTwoFactor: true);
107 |
108 | if (result.Succeeded)
109 | {
110 | Logger.LogInformation(
111 | "{Name} logged in with {LoginProvider} provider.",
112 | externalLoginInfo.Principal.Identity?.Name,
113 | externalLoginInfo.LoginProvider);
114 | RedirectManager.RedirectTo(ReturnUrl);
115 | }
116 | else if (result.IsLockedOut)
117 | {
118 | RedirectManager.RedirectTo("Account/Lockout");
119 | }
120 |
121 | // If the user does not have an account, then ask the user to create an account.
122 | if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
123 | {
124 | Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
125 | }
126 | }
127 |
128 | private async Task OnValidSubmitAsync()
129 | {
130 | var emailStore = GetEmailStore();
131 | var user = CreateUser();
132 |
133 | await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
134 | await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
135 |
136 | var result = await UserManager.CreateAsync(user);
137 | if (result.Succeeded)
138 | {
139 | result = await UserManager.AddLoginAsync(user, externalLoginInfo);
140 | if (result.Succeeded)
141 | {
142 | Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);
143 |
144 | var userId = await UserManager.GetUserIdAsync(user);
145 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
146 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
147 |
148 | var callbackUrl = NavigationManager.GetUriWithQueryParameters(
149 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
150 | new Dictionary { ["userId"] = userId, ["code"] = code });
151 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
152 |
153 | // If account confirmation is required, we need to show the link if we don't have a real email sender
154 | if (UserManager.Options.SignIn.RequireConfirmedAccount)
155 | {
156 | RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
157 | }
158 |
159 | await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
160 | RedirectManager.RedirectTo(ReturnUrl);
161 | }
162 | }
163 |
164 | message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
165 | }
166 |
167 | private ApplicationUser CreateUser()
168 | {
169 | try
170 | {
171 | return Activator.CreateInstance();
172 | }
173 | catch
174 | {
175 | throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
176 | $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
177 | }
178 | }
179 |
180 | private IUserEmailStore GetEmailStore()
181 | {
182 | if (!UserManager.SupportsUserEmail)
183 | {
184 | throw new NotSupportedException("The default UI requires a user store with email support.");
185 | }
186 | return (IUserEmailStore)UserStore;
187 | }
188 |
189 | private sealed class InputModel
190 | {
191 | [Required]
192 | [EmailAddress]
193 | public string Email { get; set; } = "";
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ForgotPassword.razor:
--------------------------------------------------------------------------------
1 | @page "/Account/ForgotPassword"
2 |
3 | @using System.ComponentModel.DataAnnotations
4 | @using System.Text
5 | @using System.Text.Encodings.Web
6 | @using Microsoft.AspNetCore.Identity
7 | @using Microsoft.AspNetCore.WebUtilities
8 | @using CleanBlazorWeb.Data
9 |
10 | @inject UserManager UserManager
11 | @inject IEmailSender EmailSender
12 | @inject NavigationManager NavigationManager
13 | @inject IdentityRedirectManager RedirectManager
14 |
15 | Forgot your password?
16 |
17 |
Forgot your password?
18 |
Enter your email.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | @code {
37 | [SupplyParameterFromForm]
38 | private InputModel Input { get; set; } = new();
39 |
40 | private async Task OnValidSubmitAsync()
41 | {
42 | var user = await UserManager.FindByEmailAsync(Input.Email);
43 | if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
44 | {
45 | // Don't reveal that the user does not exist or is not confirmed
46 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
47 | }
48 |
49 | // For more information on how to enable account confirmation and password reset please
50 | // visit https://go.microsoft.com/fwlink/?LinkID=532713
51 | var code = await UserManager.GeneratePasswordResetTokenAsync(user);
52 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
53 | var callbackUrl = NavigationManager.GetUriWithQueryParameters(
54 | NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
55 | new Dictionary { ["code"] = code });
56 |
57 | await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
58 |
59 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
60 | }
61 |
62 | private sealed class InputModel
63 | {
64 | [Required]
65 | [EmailAddress]
66 | public string Email { get; set; } = "";
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ForgotPasswordConfirmation.razor:
--------------------------------------------------------------------------------
1 | @page "/Account/ForgotPasswordConfirmation"
2 |
3 | Forgot password confirmation
4 |
5 |
Forgot password confirmation
6 |
7 | Please check your email to reset your password.
8 |
46 |
47 | @code {
48 | private string? message;
49 | private ApplicationUser user = default!;
50 |
51 | [SupplyParameterFromForm]
52 | private InputModel Input { get; set; } = new();
53 |
54 | [SupplyParameterFromQuery]
55 | private string? ReturnUrl { get; set; }
56 |
57 | [SupplyParameterFromQuery]
58 | private bool RememberMe { get; set; }
59 |
60 | protected override async Task OnInitializedAsync()
61 | {
62 | // Ensure the user has gone through the username & password screen first
63 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
64 | throw new InvalidOperationException("Unable to load two-factor authentication user.");
65 | }
66 |
67 | private async Task OnValidSubmitAsync()
68 | {
69 | var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
70 | var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
71 | var userId = await UserManager.GetUserIdAsync(user);
72 |
73 | if (result.Succeeded)
74 | {
75 | Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
76 | RedirectManager.RedirectTo(ReturnUrl);
77 | }
78 | else if (result.IsLockedOut)
79 | {
80 | Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
81 | RedirectManager.RedirectTo("Account/Lockout");
82 | }
83 | else
84 | {
85 | Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
86 | message = "Error: Invalid authenticator code.";
87 | }
88 | }
89 |
90 | private sealed class InputModel
91 | {
92 | [Required]
93 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
94 | [DataType(DataType.Text)]
95 | [Display(Name = "Authenticator code")]
96 | public string? TwoFactorCode { get; set; }
97 |
98 | [Display(Name = "Remember this machine")]
99 | public bool RememberMachine { get; set; }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/LoginWithRecoveryCode.razor:
--------------------------------------------------------------------------------
1 | @page "/Account/LoginWithRecoveryCode"
2 |
3 | @using System.ComponentModel.DataAnnotations
4 | @using Microsoft.AspNetCore.Identity
5 | @using CleanBlazorWeb.Data
6 |
7 | @inject SignInManager SignInManager
8 | @inject UserManager UserManager
9 | @inject IdentityRedirectManager RedirectManager
10 | @inject ILogger Logger
11 |
12 | Recovery code verification
13 |
14 |
Recovery code verification
15 |
16 |
17 |
18 | You have requested to log in with a recovery code. This login will not be remembered until you provide
19 | an authenticator app code at log in or disable 2FA and log in again.
20 |
21 | Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
22 | used in an authenticator app you should reset your authenticator keys.
23 |
To use an authenticator app go through the following steps:
28 |
29 |
30 |
31 | Download a two-factor authenticator app like Microsoft Authenticator for
32 | Android and
33 | iOS or
34 | Google Authenticator for
35 | Android and
36 | iOS.
37 |
38 |
39 |
40 |
Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.
47 | Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
48 | with a unique code. Enter the code in the confirmation box below.
49 |
26 | If you lose your device and don't have the recovery codes you will lose access to your account.
27 |
28 |
29 | Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
30 | used in an authenticator app you should reset your authenticator keys.
31 |
18 |
19 | If you reset your authenticator key your authenticator app will not work until you reconfigure it.
20 |
21 |
22 | This process disables 2FA until you verify your authenticator app.
23 | If you do not complete your authenticator app configuration you may lose access to your account.
24 |
25 |
26 |
27 |
31 |
32 |
33 | @code {
34 | [CascadingParameter]
35 | private HttpContext HttpContext { get; set; } = default!;
36 |
37 | private async Task OnSubmitAsync()
38 | {
39 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
40 | await UserManager.SetTwoFactorEnabledAsync(user, false);
41 | await UserManager.ResetAuthenticatorKeyAsync(user);
42 | var userId = await UserManager.GetUserIdAsync(user);
43 | Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
44 |
45 | await SignInManager.RefreshSignInAsync(user);
46 |
47 | RedirectManager.RedirectToWithStatus(
48 | "Account/Manage/EnableAuthenticator",
49 | "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.",
50 | HttpContext);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/SetPassword.razor:
--------------------------------------------------------------------------------
1 | @page "/Account/Manage/SetPassword"
2 |
3 | @using System.ComponentModel.DataAnnotations
4 | @using Microsoft.AspNetCore.Identity
5 | @using CleanBlazorWeb.Data
6 |
7 | @inject UserManager UserManager
8 | @inject SignInManager SignInManager
9 | @inject IdentityUserAccessor UserAccessor
10 | @inject IdentityRedirectManager RedirectManager
11 |
12 | Set password
13 |
14 |
Set your password
15 |
16 |
17 | You do not have a local username/password for this site. Add a local
18 | account so you can log in without an external login.
19 |
16 |
17 |
18 |
19 | @if (emailConfirmationLink is not null)
20 | {
21 |
22 | This app does not currently have a real email sender registered, see these docs for how to configure a real email sender.
23 | Normally this would be emailed: Click here to confirm your account
24 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |