Skip to content

Commit d178068

Browse files
committed
Update template passkey functionality
1 parent bb187c7 commit d178068

File tree

11 files changed

+359
-350
lines changed

11 files changed

+359
-350
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"exclude": [
136136
"BlazorWeb-CSharp/Components/Account/**",
137137
"BlazorWeb-CSharp/Data/**",
138+
"BlazorWeb-CSharp/wwwroot/BlazorWeb-CSharp.lib.module.js",
138139
"BlazorWeb-CSharp.Client/UserInfo.cs",
139140
"BlazorWeb-CSharp.Client/Pages/Auth.razor"
140141
]

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,39 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn
4949
return TypedResults.LocalRedirect($"~/{returnUrl}");
5050
});
5151

52+
accountGroup.MapPost("/PasskeyCreationOptions", async (
53+
HttpContext context,
54+
[FromServices] UserManager<ApplicationUser> userManager,
55+
[FromServices] SignInManager<ApplicationUser> signInManager) =>
56+
{
57+
var user = await userManager.GetUserAsync(context.User);
58+
if (user is null)
59+
{
60+
return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'.");
61+
}
62+
63+
var userId = await userManager.GetUserIdAsync(user);
64+
var userName = await userManager.GetUserNameAsync(user) ?? "User";
65+
var userEntity = new PasskeyUserEntity(userId, userName, displayName: userName);
66+
var passkeyCreationArgs = new PasskeyCreationArgs(userEntity);
67+
var options = await signInManager.ConfigurePasskeyCreationOptionsAsync(passkeyCreationArgs);
68+
return TypedResults.Content(options.AsJson(), contentType: "application/json");
69+
});
70+
71+
accountGroup.MapPost("/PasskeyRequestOptions", async (
72+
[FromServices] UserManager<ApplicationUser> userManager,
73+
[FromServices] SignInManager<ApplicationUser> signInManager,
74+
[FromQuery] string? email) =>
75+
{
76+
var user = string.IsNullOrEmpty(email) ? null : await userManager.FindByEmailAsync(email);
77+
var passkeyRequestArgs = new PasskeyRequestArgs<ApplicationUser>
78+
{
79+
User = user,
80+
};
81+
var options = await signInManager.ConfigurePasskeyRequestOptionsAsync(passkeyRequestArgs);
82+
return TypedResults.Content(options.AsJson(), contentType: "application/json");
83+
});
84+
5285
var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization();
5386

5487
manageGroup.MapPost("/LinkExternalLogin", async (

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

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
<div class="row">
1818
<div class="col-lg-6">
1919
<section>
20-
<PasskeyHandler CurrentRequestOptions="@currentPasskeyRequestOptions" OnResponse="LoginUserWithPasskey" OnError="SetPasskeyError" />
2120
<StatusMessage Message="@errorMessage" />
2221
<EditForm EditContext="editContext" method="post" OnSubmit="LoginUser" FormName="login">
2322
<DataAnnotationsValidator />
@@ -41,12 +40,12 @@
4140
</label>
4241
</div>
4342
<div>
44-
<button type="submit" class="w-100 btn btn-lg btn-primary" disabled="@(currentPasskeyRequestOptions is not null)">Log in</button>
43+
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
4544
</div>
4645
<hr />
4746
<div class="d-flex flex-column">
4847
<span class="text-secondary mx-auto mt-2">OR</span>
49-
<button type="submit" name="Input.UsePasskey" value="true" class="btn btn-link mx-auto" disabled="@(currentPasskeyRequestOptions is not null)">Log in with a passkey</button>
48+
<PasskeySubmit Operation="PasskeyOperation.Request" Name="Input.Passkey" EmailName="Input.Email" class="btn btn-link mx-auto">Log in with a passkey</PasskeySubmit>
5049
</div>
5150
<hr />
5251
<div>
@@ -74,14 +73,12 @@
7473

7574
@code {
7675
private string? errorMessage;
77-
7876
private EditContext editContext = default!;
79-
private string? currentPasskeyRequestOptions;
8077

8178
[CascadingParameter]
8279
private HttpContext HttpContext { get; set; } = default!;
8380

84-
[SupplyParameterFromForm(FormName = "login")]
81+
[SupplyParameterFromForm]
8582
private InputModel Input { get; set; } = new();
8683

8784
[SupplyParameterFromQuery]
@@ -100,32 +97,38 @@
10097

10198
public async Task LoginUser()
10299
{
103-
// When performing passkey sign-in, don't perform form validation.
104-
// If provided, we use the email as a hint to suggest which user is likely being signed in.
105-
if (Input.UsePasskey)
100+
if (!string.IsNullOrEmpty(Input.Passkey?.Error))
106101
{
107-
var user = Input.Email is { Length: > 0 } email
108-
? await UserManager.FindByEmailAsync(email)
109-
: null;
110-
111-
var passkeyRequestArgs = new PasskeyRequestArgs<ApplicationUser>
112-
{
113-
User = user,
114-
};
115-
var options = await SignInManager.ConfigurePasskeyRequestOptionsAsync(passkeyRequestArgs);
116-
currentPasskeyRequestOptions = options.AsJson();
102+
errorMessage = $"Error: Could not log in using the provided passkey: {Input.Passkey.Error}";
117103
return;
118104
}
119105

120-
// If doing a password sign-in, validate the form.
121-
if (!editContext.Validate())
106+
SignInResult result;
107+
if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson))
122108
{
123-
return;
109+
// When performing passkey sign-in, don't perform form validation.
110+
var options = await SignInManager.RetrievePasskeyRequestOptionsAsync();
111+
if (options is null)
112+
{
113+
errorMessage = "Error: Could not complete passkey login. Please try again.";
114+
return;
115+
}
116+
117+
result = await SignInManager.PasskeySignInAsync(Input.Passkey.CredentialJson, options);
118+
}
119+
else
120+
{
121+
// If doing a password sign-in, validate the form.
122+
if (!editContext.Validate())
123+
{
124+
return;
125+
}
126+
127+
// This doesn't count login failures towards account lockout
128+
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
129+
result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
124130
}
125131

126-
// This doesn't count login failures towards account lockout
127-
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
128-
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
129132
if (result.Succeeded)
130133
{
131134
Logger.LogInformation("User logged in.");
@@ -148,33 +151,6 @@
148151
}
149152
}
150153

151-
public async Task LoginUserWithPasskey(string responseJson)
152-
{
153-
var options = await SignInManager.RetrievePasskeyRequestOptionsAsync();
154-
if (options is null)
155-
{
156-
errorMessage = "Error: Could not complete passkey login. Please try again.";
157-
return;
158-
}
159-
160-
var result = await SignInManager.PasskeySignInAsync(responseJson, options);
161-
if (result.Succeeded)
162-
{
163-
Logger.LogInformation("User logged in.");
164-
RedirectManager.RedirectTo(ReturnUrl);
165-
}
166-
else
167-
{
168-
errorMessage = "Error: Could not log in using the provided passkey.";
169-
return;
170-
}
171-
}
172-
173-
public void SetPasskeyError(string? error)
174-
{
175-
errorMessage = $"Error: Could not log in using the provided passkey{(string.IsNullOrEmpty(error) ? "." : $": {error}")}";
176-
}
177-
178154
private sealed class InputModel
179155
{
180156
[Required]
@@ -188,6 +164,6 @@
188164
[Display(Name = "Remember me?")]
189165
public bool RememberMe { get; set; }
190166

191-
public bool UsePasskey { get; set; }
167+
public PasskeyInputModel? Passkey { get; set; }
192168
}
193169
}

0 commit comments

Comments
 (0)