Skip to content

Commit a595745

Browse files
authored
Merge pull request #7 from damienbod/feature/passkeys-update
Update packages and passkeys implementation
2 parents cf2b349 + 36453dd commit a595745

106 files changed

Lines changed: 3389 additions & 3412 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MicrosoftEntraIDMultiApis/MultiMicrosoftEntraIDWebApi/MultiMicrosoftEntraIDWebApi.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.Identity.Web" Version="4.1.1" />
10+
<PackageReference Include="Microsoft.Identity.Web" Version="4.3.0" />
1111
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="7.2.0" />
1212
<PackageReference Include="OpenIddict.Validation.SystemNetHttp" Version="7.2.0" />
13-
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
13+
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.4" />
1414

1515
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
1616
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />

MicrosoftEntraIDMultiApis/TestMultiApis/Pages/Shared/_Layout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
<footer class="border-top footer text-muted">
4646
<div class="container">
47-
&copy; 2025 - Razor Microsoft Entra ID
47+
&copy; 2026 - Razor Microsoft Entra ID
4848
</div>
4949
</footer>
5050

MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.Identity.Web" Version="4.1.1" />
12-
<PackageReference Include="Microsoft.Identity.Web.UI" Version="4.1.1" />
13-
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.3.0" />
14-
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" Version="1.3.0" />
11+
<PackageReference Include="Microsoft.Identity.Web" Version="4.3.0" />
12+
<PackageReference Include="Microsoft.Identity.Web.UI" Version="4.3.0" />
13+
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.3.1" />
14+
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" Version="1.3.1" />
1515

1616
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
1717
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />

MultiIdentityProvider/IdentityProvider/Areas/Identity/IdentityHostingStartup.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
[assembly: HostingStartup(typeof(OpeniddictServer.Areas.Identity.IdentityHostingStartup))]
2-
namespace OpeniddictServer.Areas.Identity
1+
[assembly: HostingStartup(typeof(IdentityProvider.Areas.Identity.IdentityHostingStartup))]
2+
namespace IdentityProvider.Areas.Identity
33
{
44
public class IdentityHostingStartup : IHostingStartup
55
{
Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
11
@page
2+
@using IdentityProvider
3+
@using IdentityProvider.Areas.Identity
4+
@using IdentityProvider.Areas.Identity.Pages
5+
@using IdentityProvider.Areas.Identity.Pages.Account
6+
@using IdentityProvider.Data
7+
@using IdentityProvider.Passkeys
28
@model LoginModel
39

410
@{
511
ViewData["Title"] = "Log in";
612
}
713

14+
815
<h1>@ViewData["Title"]</h1>
916
<div class="row">
1017
<div class="col-md-4">
1118
<section>
1219
<form id="account" method="post">
13-
<h3>Use a local account to log in.</h3>
20+
<h2>Use a local account to log in.</h2>
1421
<hr />
15-
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
16-
17-
<div class="mb-3">
18-
<label asp-for="Input.Email" class="form-label"></label>
19-
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" aria-describedby="emailHelp" />
20-
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
22+
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
23+
<div class="form-floating mb-3">
24+
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
25+
<label asp-for="Input.Email" class="form-label">Email</label>
2126
<span asp-validation-for="Input.Email" class="text-danger"></span>
2227
</div>
23-
<div class="mb-3">
24-
<label asp-for="Input.Password" class="form-label"></label>
25-
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
28+
<div class="form-floating mb-3">
29+
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
30+
<label asp-for="Input.Password" class="form-label">Password</label>
2631
<span asp-validation-for="Input.Password" class="text-danger"></span>
2732
</div>
28-
<div class="mb-3 form-check">
29-
<input type="checkbox" class="form-check-input" asp-for="Input.RememberMe">
30-
<label class="form-check-label" asp-for="Input.RememberMe">@Html.DisplayNameFor(m => m.Input.RememberMe)</label>
33+
<div class="checkbox mb-3">
34+
<label asp-for="Input.RememberMe" class="form-label">
35+
<input class="form-check-input" asp-for="Input.RememberMe" />
36+
@Html.DisplayNameFor(m => m.Input.RememberMe)
37+
</label>
3138
</div>
32-
3339
<div>
3440
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
3541
</div>
36-
<br />
3742
<div>
3843
<p>
3944
<a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
@@ -45,6 +50,13 @@
4550
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
4651
</p>
4752
</div>
53+
54+
<hr />
55+
56+
<passkey-submit operation="@PasskeyOperation.Request" name="Input.Passkey" email-name="Input.Email" class="btn btn-outline-primary position-relative" formnovalidate>
57+
Log in with a passkey
58+
</passkey-submit>
59+
4860
</form>
4961
</section>
5062
</div>
@@ -57,8 +69,10 @@
5769
{
5870
<div>
5971
<p>
60-
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
61-
about setting up this ASP.NET application to support logging in via external services</a>.
72+
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">
73+
article
74+
about setting up this ASP.NET application to support logging in via external services
75+
</a>.
6276
</p>
6377
</div>
6478
}
@@ -67,9 +81,9 @@
6781
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
6882
<div>
6983
<p>
70-
@foreach (var provider in Model.ExternalLogins)
84+
@foreach (var provider in Model.ExternalLogins!)
7185
{
72-
<button type="submit" class="w-100 btn btn-lg btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
86+
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
7387
}
7488
</p>
7589
</div>
@@ -81,6 +95,7 @@
8195
</div>
8296

8397
@section Scripts {
84-
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
85-
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>
98+
<partial name="_ValidationScriptsPartial" />
99+
<script src="~/js/passkey-submit.js" asp-append-version="true"></script>
86100
}
101+

MultiIdentityProvider/IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml.cs

Lines changed: 101 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,143 +2,142 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
#nullable disable
44

5-
using Fido2Identity;
65
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Authorization;
77
using Microsoft.AspNetCore.Identity;
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.AspNetCore.Mvc.RazorPages;
10-
using OpeniddictServer.Data;
10+
using IdentityProvider.Data;
1111
using System.ComponentModel.DataAnnotations;
1212

13-
namespace OpeniddictServer.Areas.Identity.Pages.Account
13+
namespace IdentityProvider.Areas.Identity.Pages.Account;
14+
15+
[AllowAnonymous]
16+
public class LoginModel : PageModel
1417
{
15-
public class LoginModel : PageModel
16-
{
17-
private readonly SignInManager<ApplicationUser> _signInManager;
18-
private readonly Fido2Store _fido2Store;
19-
private readonly ILogger<LoginModel> _logger;
18+
private readonly SignInManager<ApplicationUser> _signInManager;
19+
private readonly ILogger<LoginModel> _logger;
2020

21-
public LoginModel(SignInManager<ApplicationUser> signInManager,
22-
Fido2Store fido2Store,
23-
ILogger<LoginModel> logger)
24-
{
25-
_signInManager = signInManager;
26-
_fido2Store = fido2Store;
27-
_logger = logger;
28-
}
21+
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger)
22+
{
23+
_signInManager = signInManager;
24+
_logger = logger;
25+
}
2926

27+
/// <summary>
28+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
29+
/// directly from your code. This API may change or be removed in future releases.
30+
/// </summary>
31+
[BindProperty]
32+
public InputModel Input { get; set; }
33+
34+
/// <summary>
35+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
36+
/// directly from your code. This API may change or be removed in future releases.
37+
/// </summary>
38+
public IList<AuthenticationScheme> ExternalLogins { get; set; }
39+
40+
/// <summary>
41+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
42+
/// directly from your code. This API may change or be removed in future releases.
43+
/// </summary>
44+
public string ReturnUrl { get; set; }
45+
46+
/// <summary>
47+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
48+
/// directly from your code. This API may change or be removed in future releases.
49+
/// </summary>
50+
[TempData]
51+
public string ErrorMessage { get; set; }
52+
53+
/// <summary>
54+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
55+
/// directly from your code. This API may change or be removed in future releases.
56+
/// </summary>
57+
public class InputModel
58+
{
3059
/// <summary>
3160
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
3261
/// directly from your code. This API may change or be removed in future releases.
3362
/// </summary>
34-
[BindProperty]
35-
public InputModel Input { get; set; }
63+
[Required]
64+
[EmailAddress]
65+
public string Email { get; set; }
3666

3767
/// <summary>
3868
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
3969
/// directly from your code. This API may change or be removed in future releases.
4070
/// </summary>
41-
public IList<AuthenticationScheme> ExternalLogins { get; set; }
71+
[Required]
72+
[DataType(DataType.Password)]
73+
public string Password { get; set; }
4274

4375
/// <summary>
4476
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
4577
/// directly from your code. This API may change or be removed in future releases.
4678
/// </summary>
47-
public string ReturnUrl { get; set; }
79+
[Display(Name = "Remember me?")]
80+
public bool RememberMe { get; set; }
4881

49-
/// <summary>
50-
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
51-
/// directly from your code. This API may change or be removed in future releases.
52-
/// </summary>
53-
[TempData]
54-
public string ErrorMessage { get; set; }
82+
public PasskeyInputModel Passkey { get; set; }
83+
}
5584

56-
/// <summary>
57-
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
58-
/// directly from your code. This API may change or be removed in future releases.
59-
/// </summary>
60-
public class InputModel
85+
public async Task OnGetAsync(string returnUrl = null)
86+
{
87+
if (!string.IsNullOrEmpty(ErrorMessage))
6188
{
62-
/// <summary>
63-
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
64-
/// directly from your code. This API may change or be removed in future releases.
65-
/// </summary>
66-
[Required]
67-
[EmailAddress]
68-
public string Email { get; set; }
69-
70-
/// <summary>
71-
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
72-
/// directly from your code. This API may change or be removed in future releases.
73-
/// </summary>
74-
[Required]
75-
[DataType(DataType.Password)]
76-
public string Password { get; set; }
77-
78-
/// <summary>
79-
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
80-
/// directly from your code. This API may change or be removed in future releases.
81-
/// </summary>
82-
[Display(Name = "Remember me?")]
83-
public bool RememberMe { get; set; }
89+
ModelState.AddModelError(string.Empty, ErrorMessage);
8490
}
8591

86-
public async Task OnGetAsync(string returnUrl = null)
87-
{
88-
if (!string.IsNullOrEmpty(ErrorMessage))
89-
{
90-
ModelState.AddModelError(string.Empty, ErrorMessage);
91-
}
92+
returnUrl ??= Url.Content("~/");
93+
94+
// Clear the existing external cookie to ensure a clean login process
95+
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
96+
97+
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
98+
99+
ReturnUrl = returnUrl;
100+
}
101+
102+
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
103+
{
104+
returnUrl ??= Url.Content("~/");
92105

93-
returnUrl ??= Url.Content("~/");
106+
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
94107

95-
// Clear the existing external cookie to ensure a clean login process
96-
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
108+
Microsoft.AspNetCore.Identity.SignInResult result = null;
97109

98-
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
110+
if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson))
111+
{
112+
// When performing passkey sign-in, don't perform form validation.
113+
ModelState.Clear();
99114

100-
ReturnUrl = returnUrl;
115+
result = await _signInManager.PasskeySignInAsync(Input.Passkey.CredentialJson);
116+
}
117+
else if (ModelState.IsValid)
118+
{
119+
// This doesn't count login failures towards account lockout
120+
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
121+
result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
101122
}
102123

103-
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
124+
if (result.Succeeded)
125+
{
126+
_logger.LogInformation("User logged in.");
127+
return LocalRedirect(returnUrl);
128+
}
129+
if (result.RequiresTwoFactor)
130+
{
131+
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
132+
}
133+
if (result.IsLockedOut)
134+
{
135+
_logger.LogWarning("User account locked out.");
136+
return RedirectToPage("./Lockout");
137+
}
138+
else
104139
{
105-
returnUrl ??= Url.Content("~/");
106-
107-
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
108-
109-
if (ModelState.IsValid)
110-
{
111-
// This doesn't count login failures towards account lockout
112-
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
113-
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
114-
if (result.Succeeded)
115-
{
116-
_logger.LogInformation("User logged in.");
117-
return LocalRedirect(returnUrl);
118-
}
119-
if (result.RequiresTwoFactor)
120-
{
121-
var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(Input.Email);
122-
if (fido2ItemExistsForUser.Count > 0)
123-
{
124-
return RedirectToPage("./LoginFido2Mfa", new { ReturnUrl = returnUrl, Input.RememberMe });
125-
}
126-
127-
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
128-
}
129-
if (result.IsLockedOut)
130-
{
131-
_logger.LogWarning("User account locked out.");
132-
return RedirectToPage("./Lockout");
133-
}
134-
else
135-
{
136-
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
137-
return Page();
138-
}
139-
}
140-
141-
// If we got this far, something failed, redisplay form
140+
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
142141
return Page();
143142
}
144143
}

0 commit comments

Comments
 (0)