Skip to content

Commit 2ba1d1e

Browse files
committed
Add option for persisting sign-in when using passkeys.
1 parent 14fe0b2 commit 2ba1d1e

File tree

6 files changed

+59
-11
lines changed

6 files changed

+59
-11
lines changed

ControlR.Web.Server/Components/Account/Pages/Login.razor

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@using ControlR.Web.Server.Components.Account.Shared
44

55
@inject SignInManager<AppUser> SignInManager
6+
@inject PasskeySignInManager PasskeySignInManager
67
@inject ILogger<Login> Logger
78
@inject NavigationManager NavigationManager
89
@inject IdentityRedirectManager RedirectManager
@@ -38,13 +39,10 @@
3839
</MudStaticCheckBox>
3940
</MudItem>
4041
<MudItem xs="12" Class="d-flex gap-4">
41-
<MudStaticButton Variant="Variant.Outlined" Color="Color.Primary"
42-
FormAction="FormAction.Submit">Log in</MudStaticButton>
43-
<PasskeySubmit Operation="PasskeyOperation.Request"
44-
Name="Input.Passkey"
45-
EmailName="Input.Email"
46-
Variant="Variant.Outlined"
47-
Color="Color.Secondary">
42+
<MudStaticButton Variant="Variant.Outlined" Color="Color.Primary" FormAction="FormAction.Submit">Log
43+
in</MudStaticButton>
44+
<PasskeySubmit Operation="PasskeyOperation.Request" Name="Input.Passkey" EmailName="Input.Email"
45+
Variant="Variant.Outlined" Color="Color.Secondary">
4846
Sign in with a passkey
4947
</PasskeySubmit>
5048
</MudItem>
@@ -115,11 +113,17 @@
115113

116114
public async Task LoginUser()
117115
{
118-
Microsoft.AspNetCore.Identity.SignInResult result;
116+
if (!string.IsNullOrEmpty(Input.Passkey?.Error))
117+
{
118+
_errorMessage = $"Error: {Input.Passkey.Error}";
119+
return;
120+
}
121+
122+
SignInResult result;
119123

120124
if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson))
121125
{
122-
result = await SignInManager.PasskeySignInAsync(Input.Passkey.CredentialJson);
126+
result = await PasskeySignInManager.PasskeySignInAsync(Input.Passkey.CredentialJson);
123127
}
124128
else
125129
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using Microsoft.AspNetCore.Authentication;
3+
4+
namespace ControlR.Web.Server.Components.Account;
5+
6+
public class PasskeySignInManager(
7+
UserManager<AppUser> userManager,
8+
IOptionsMonitor<AppOptions> appOptions,
9+
IHttpContextAccessor contextAccessor,
10+
IUserClaimsPrincipalFactory<AppUser> claimsFactory,
11+
IOptions<IdentityOptions> optionsAccessor,
12+
ILogger<SignInManager<AppUser>> logger,
13+
IAuthenticationSchemeProvider schemes,
14+
IUserConfirmation<AppUser> confirmation)
15+
: SignInManager<AppUser>(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
16+
{
17+
public override async Task<SignInResult> PasskeySignInAsync(string credentialJson)
18+
{
19+
ArgumentException.ThrowIfNullOrEmpty(credentialJson);
20+
21+
var assertionResult = await PerformPasskeyAssertionAsync(credentialJson);
22+
if (!assertionResult.Succeeded)
23+
{
24+
return SignInResult.Failed;
25+
}
26+
27+
// After a successful assertion, we need to update the passkey so that it has the latest
28+
// sign count and authenticator data.
29+
var setPasskeyResult = await UserManager.AddOrUpdatePasskeyAsync(assertionResult.User, assertionResult.Passkey);
30+
if (!setPasskeyResult.Succeeded)
31+
{
32+
return SignInResult.Failed;
33+
}
34+
35+
var persistLogin = appOptions.CurrentValue.PersistPasskeyLogin;
36+
return await SignInOrTwoFactorAsync(assertionResult.User, isPersistent: persistLogin, bypassTwoFactor: true);
37+
}
38+
}

ControlR.Web.Server/Options/AppOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class AppOptions
1818
public long MaxFileTransferSize { get; init; } = 100 * 1024 * 1024; // 100 MB default
1919
public string? MicrosoftClientId { get; init; }
2020
public string? MicrosoftClientSecret { get; init; }
21+
public bool PersistPasskeyLogin { get; init; }
2122
public bool RequireUserEmailConfirmation { get; init; }
2223
public bool SmtpCheckCertificateRevocation { get; init; } = true;
2324
public string? SmtpDisplayName { get; init; }

ControlR.Web.Server/Startup/WebApplicationBuilderExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ public static async Task<IHostApplicationBuilder> AddControlrServer(
143143

144144
authBuilder.AddIdentityCookies();
145145

146-
// Add this to your WebApplicationBuilderExtensions.cs
147146
builder.Services.ConfigureApplicationCookie(options =>
148147
{
149148
options.Events.OnRedirectToLogin = context =>
@@ -212,7 +211,9 @@ public static async Task<IHostApplicationBuilder> AddControlrServer(
212211
.AddSignInManager()
213212
.AddDefaultTokenProviders();
214213

215-
builder.Services.AddScoped<IUserCreator, UserCreator>();
214+
builder.Services
215+
.AddScoped<PasskeySignInManager>()
216+
.AddScoped<IUserCreator, UserCreator>();
216217

217218
// Configure DataProtection.
218219
builder.Services

ControlR.Web.Server/appsettings.Development.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"AllowAgentsToSelfBootstrap": true,
2020
"AuthenticatorIssuerName": "ControlR",
2121
"EnablePublicRegistration": false,
22+
"PersistPasskeyLogin": true,
2223
"MaxFileTransferSize": -1,
2324
"EnableCloudflareProxySupport": false,
2425
"MicrosoftClientId": "",

docker-compose/docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ services:
3737
# The name that appears in TOTP authenticator apps.
3838
ControlR_AppOptions__AuthenticatorIssuerName: "ControlR"
3939

40+
# If enabled, signing in with a passkey will effectively add the "remember me" option.
41+
ControlR_AppOptions__PersistPasskeyLogin: false
42+
4043
# Automatically obtain Cloudflare IPs from https://www.cloudflare.com/ips-v4
4144
# and add them to the KnownNetworks list for forwarded headers.
4245
ControlR_AppOptions__EnableCloudflareProxySupport: false

0 commit comments

Comments
 (0)