Skip to content

Commit d85f2a6

Browse files
halter73MackinnonBuck
authored andcommitted
Include updates from halter73/BlazorWebIdentity#2
1 parent 10e7a6f commit d85f2a6

36 files changed

+1724
-181
lines changed

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@
114114
"condition": "(!IndividualLocalAuth)",
115115
"exclude": [
116116
"BlazorWeb-CSharp/Components/Pages/Account/**",
117+
"BlazorWeb-CSharp/Components/Identity/**",
117118
"BlazorWeb-CSharp/Data/**",
119+
"BlazorWeb-CSharp/Identity/**",
118120
"BlazorWeb-CSharp/PersistingAuthenticationStateProvider.cs",
119121
"BlazorWeb-CSharp.Client/PersistedAuthenticationStateProvider.cs",
120122
"BlazorWeb-CSharp.Client/UserInfo.cs",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@using Microsoft.AspNetCore.Authentication
2+
@using Microsoft.AspNetCore.Identity
3+
@using BlazorWeb_CSharp.Components.Pages.Account
4+
@using BlazorWeb_CSharp.Data
5+
6+
@inject SignInManager<ApplicationUser> SignInManager
7+
@inject NavigationManager NavigationManager
8+
9+
@if ((_externalLogins?.Count ?? 0) == 0)
10+
{
11+
<div>
12+
<p>
13+
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
14+
about setting up this ASP.NET application to support logging in via external services</a>.
15+
</p>
16+
</div>
17+
}
18+
else
19+
{
20+
<form id="external-account" class="form-horizontal" action="/Account/PerformExternalLogin" method="post">
21+
<div>
22+
<AntiforgeryToken />
23+
<input type="hidden" name="ReturnUrl" value="@ReturnUrl" />
24+
<p>
25+
@foreach (var provider in _externalLogins!)
26+
{
27+
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
28+
}
29+
</p>
30+
</div>
31+
</form>
32+
}
33+
34+
@code {
35+
private IList<AuthenticationScheme>? _externalLogins;
36+
37+
[SupplyParameterFromQuery]
38+
private string ReturnUrl { get; set; } = default!;
39+
40+
protected override async Task OnInitializedAsync()
41+
{
42+
ReturnUrl ??= "/";
43+
44+
_externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@{
2+
var message = Message ?? MessageFromQuery;
3+
}
4+
5+
@if (!string.IsNullOrEmpty(message))
6+
{
7+
var statusMessageClass = message.StartsWith("Error") ? "danger" : "success";
8+
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
9+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
10+
@message
11+
</div>
12+
}
13+
14+
@code {
15+
[Parameter]
16+
public string? Message { get; set; }
17+
18+
[SupplyParameterFromQuery(Name = "Message")]
19+
public string? MessageFromQuery { get; set; }
20+
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ConfirmEmail.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
{
2727
if (UserId == null || Code == null)
2828
{
29-
NavigationManager.NavigateTo("/");
29+
NavigationManager.RedirectTo("/");
3030
}
3131
else
3232
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@page "/Account/ConfirmEmailChange"
2+
3+
@using System.Text
4+
@using Microsoft.AspNetCore.Identity
5+
@using Microsoft.AspNetCore.WebUtilities
6+
@using BlazorWeb_CSharp.Data
7+
8+
@inject UserManager<ApplicationUser> UserManager
9+
@inject SignInManager<ApplicationUser> SignInManager
10+
@inject UserAccessor UserAccessor
11+
@inject NavigationManager NavigationManager
12+
13+
<PageTitle>Confirm email change</PageTitle>
14+
15+
<h1>Confirm email change</h1>
16+
17+
<StatusMessage Message="@_message" />
18+
19+
@code {
20+
private string? _message;
21+
private ApplicationUser _user = default!;
22+
23+
[SupplyParameterFromQuery]
24+
private string? UserId { get; set; }
25+
26+
[SupplyParameterFromQuery]
27+
private string? Email { get; set; }
28+
29+
[SupplyParameterFromQuery]
30+
private string? Code { get; set; }
31+
32+
protected override async Task OnInitializedAsync()
33+
{
34+
if (UserId is null || Email is null || Code is null)
35+
{
36+
NavigationManager.RedirectTo(
37+
"/Account/Login",
38+
new() { ["Message"] = "Error: Invalid email change confirmation link." });
39+
return;
40+
}
41+
42+
_user = await UserAccessor.GetRequiredUserAsync();
43+
44+
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
45+
var result = await UserManager.ChangeEmailAsync(_user, Email, code);
46+
if (!result.Succeeded)
47+
{
48+
_message = "Error changing email.";
49+
return;
50+
}
51+
52+
// In our UI email and user name are one and the same, so when we update the email
53+
// we need to update the user name.
54+
var setUserNameResult = await UserManager.SetUserNameAsync(_user, Email);
55+
if (!setUserNameResult.Succeeded)
56+
{
57+
_message = "Error changing user name.";
58+
return;
59+
}
60+
61+
await SignInManager.RefreshSignInAsync(_user);
62+
_message = "Thank you for confirming your email change.";
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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.Identity.UI.Services
9+
@using Microsoft.AspNetCore.WebUtilities
10+
@using BlazorWeb_CSharp.Data
11+
12+
@inject SignInManager<ApplicationUser> SignInManager
13+
@inject UserManager<ApplicationUser> UserManager
14+
@inject IUserStore<ApplicationUser> UserStore
15+
@inject IEmailSender EmailSender
16+
@inject NavigationManager NavigationManager
17+
@inject ILogger<ExternalLogin> Logger
18+
19+
@{
20+
var providerDisplayName = _externalLoginInfo.ProviderDisplayName;
21+
}
22+
23+
<PageTitle>Register</PageTitle>
24+
25+
<StatusMessage Message="@_message" />
26+
<h1>Register</h1>
27+
<h2 id="external-login-title">Associate your @providerDisplayName account.</h2>
28+
<hr />
29+
30+
<p id="external-login-description" class="text-info">
31+
You've successfully authenticated with <strong>@providerDisplayName</strong>.
32+
Please enter an email address for this site below and click the Register button to finish
33+
logging in.
34+
</p>
35+
36+
<div class="row">
37+
<div class="col-md-4">
38+
<EditForm id="confirmation-form" Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
39+
<DataAnnotationsValidator />
40+
<ValidationSummary />
41+
<div class="form-floating mb-3">
42+
<InputText id="email" @bind-Value="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email." />
43+
<label for="email" class="form-label">Email</label>
44+
<ValidationMessage For="() => Input.Email" />
45+
</div>
46+
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
47+
</EditForm>
48+
</div>
49+
</div>
50+
51+
@code {
52+
public const string LoginCallbackAction = "LoginCallback";
53+
54+
private string? _message;
55+
private ExternalLoginInfo _externalLoginInfo = default!;
56+
private IUserEmailStore<ApplicationUser> _emailStore = default!;
57+
58+
[SupplyParameterFromQuery]
59+
private string? RemoteError { get; set; }
60+
61+
[CascadingParameter]
62+
public HttpContext HttpContext { get; set; } = default!;
63+
64+
[SupplyParameterFromForm]
65+
private InputModel Input { get; set; } = default!;
66+
67+
[SupplyParameterFromQuery]
68+
private string ReturnUrl { get; set; } = default!;
69+
70+
[SupplyParameterFromQuery]
71+
private string? Action { get; set; } = default!;
72+
73+
protected override async Task OnInitializedAsync()
74+
{
75+
Input ??= new();
76+
ReturnUrl ??= "/";
77+
78+
if (RemoteError is not null)
79+
{
80+
NavigationManager.RedirectTo(
81+
"/Account/Login",
82+
new() { ["Message"] = "Error from external provider: " + RemoteError });
83+
return;
84+
}
85+
86+
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
87+
if (externalLoginInfo is null)
88+
{
89+
NavigationManager.RedirectTo(
90+
"/Account/Login",
91+
new() { ["Message"] = "Error loading external login information." });
92+
return;
93+
}
94+
95+
_externalLoginInfo = externalLoginInfo;
96+
_emailStore = GetEmailStore();
97+
98+
if (HttpMethods.IsGet(HttpContext.Request.Method))
99+
{
100+
if (Action == LoginCallbackAction)
101+
{
102+
await OnLoginCallbackAsync();
103+
return;
104+
}
105+
106+
// We should only reach this page via the login callback, so redirect back to
107+
// the login page if we get here some other way.
108+
NavigationManager.RedirectTo("/Account/Login");
109+
return;
110+
}
111+
}
112+
113+
private async Task OnLoginCallbackAsync()
114+
{
115+
// Sign in the user with this external login provider if the user already has a login.
116+
var result = await SignInManager.ExternalLoginSignInAsync(
117+
_externalLoginInfo.LoginProvider,
118+
_externalLoginInfo.ProviderKey,
119+
isPersistent: false,
120+
bypassTwoFactor: true);
121+
if (result.Succeeded)
122+
{
123+
Logger.LogInformation(
124+
"{Name} logged in with {LoginProvider} provider.",
125+
_externalLoginInfo.Principal.Identity?.Name,
126+
_externalLoginInfo.LoginProvider);
127+
NavigationManager.RedirectTo(ReturnUrl);
128+
return;
129+
}
130+
131+
if (result.IsLockedOut)
132+
{
133+
NavigationManager.RedirectTo("/Account/Lockout");
134+
return;
135+
}
136+
137+
// If the user does not have an account, then ask the user to create an account.
138+
if (_externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
139+
{
140+
Input.Email = _externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email);
141+
}
142+
}
143+
144+
private async Task OnValidSubmitAsync()
145+
{
146+
var user = CreateUser();
147+
148+
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
149+
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
150+
151+
var result = await UserManager.CreateAsync(user);
152+
if (result.Succeeded)
153+
{
154+
result = await UserManager.AddLoginAsync(user, _externalLoginInfo);
155+
if (result.Succeeded)
156+
{
157+
Logger.LogInformation("User created an account using {Name} provider.", _externalLoginInfo.LoginProvider);
158+
159+
var userId = await UserManager.GetUserIdAsync(user);
160+
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
161+
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
162+
163+
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
164+
$"{NavigationManager.BaseUri}Account/ConfirmEmail",
165+
new Dictionary<string, object?> { { "userId", userId }, { "code", code } });
166+
await EmailSender.SendEmailAsync(Input.Email!, "Confirm your email",
167+
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
168+
169+
// If account confirmation is required, we need to show the link if we don't have a real email sender
170+
if (UserManager.Options.SignIn.RequireConfirmedAccount)
171+
{
172+
NavigationManager.RedirectTo("/Account/RegisterConfirmation", new() { ["Email"] = Input.Email });
173+
return;
174+
}
175+
176+
await SignInManager.SignInAsync(user, isPersistent: false, _externalLoginInfo.LoginProvider);
177+
NavigationManager.RedirectTo(ReturnUrl);
178+
return;
179+
}
180+
}
181+
else
182+
{
183+
_message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
184+
}
185+
}
186+
187+
private ApplicationUser CreateUser()
188+
{
189+
try
190+
{
191+
return Activator.CreateInstance<ApplicationUser>();
192+
}
193+
catch
194+
{
195+
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
196+
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
197+
}
198+
}
199+
200+
private IUserEmailStore<ApplicationUser> GetEmailStore()
201+
{
202+
if (!UserManager.SupportsUserEmail)
203+
{
204+
throw new NotSupportedException("The default UI requires a user store with email support.");
205+
}
206+
return (IUserEmailStore<ApplicationUser>)UserStore;
207+
}
208+
209+
private sealed class InputModel
210+
{
211+
[Required]
212+
[EmailAddress]
213+
public string? Email { get; set; }
214+
}
215+
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Account/ForgotPassword.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
4444
{
4545
// Don't reveal that the user does not exist or is not confirmed
46-
NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation");
46+
NavigationManager.RedirectTo("/Account/ForgotPasswordConfirmation");
4747
return;
4848
}
4949

@@ -52,15 +52,15 @@
5252
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
5353
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
5454
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
55-
$"{NavigationManager.BaseUri}Identity/Account/ResetPassword",
55+
$"{NavigationManager.BaseUri}Account/ResetPassword",
5656
new Dictionary<string, object?> { { "code", code } });
5757

5858
await EmailSender.SendEmailAsync(
5959
Input.Email,
6060
"Reset Password",
6161
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
6262

63-
NavigationManager.NavigateTo("/Account/ForgotPasswordConfirmation");
63+
NavigationManager.RedirectTo("/Account/ForgotPasswordConfirmation");
6464
}
6565

6666
private sealed class InputModel

0 commit comments

Comments
 (0)