Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Identity.Web" Version="4.1.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="4.3.0" />
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="7.2.0" />
<PackageReference Include="OpenIddict.Validation.SystemNetHttp" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.4" />

<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - Razor Microsoft Entra ID
&copy; 2026 - Razor Microsoft Entra ID
</div>
</footer>

Expand Down
8 changes: 4 additions & 4 deletions MicrosoftEntraIDMultiApis/TestMultiApis/TestMultiApis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Identity.Web" Version="4.1.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="4.1.1" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.3.0" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" Version="1.3.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="4.3.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="4.3.0" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.3.1" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders.TagHelpers" Version="1.3.1" />

<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[assembly: HostingStartup(typeof(OpeniddictServer.Areas.Identity.IdentityHostingStartup))]
namespace OpeniddictServer.Areas.Identity
[assembly: HostingStartup(typeof(IdentityProvider.Areas.Identity.IdentityHostingStartup))]
namespace IdentityProvider.Areas.Identity
{
public class IdentityHostingStartup : IHostingStartup
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
@page
@using IdentityProvider
@using IdentityProvider.Areas.Identity
@using IdentityProvider.Areas.Identity.Pages
@using IdentityProvider.Areas.Identity.Pages.Account
@using IdentityProvider.Data
@using IdentityProvider.Passkeys
@model LoginModel

@{
ViewData["Title"] = "Log in";
}


<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<section>
<form id="account" method="post">
<h3>Use a local account to log in.</h3>
<h2>Use a local account to log in.</h2>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>

<div class="mb-3">
<label asp-for="Input.Email" class="form-label"></label>
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" aria-describedby="emailHelp" />
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label asp-for="Input.Email" class="form-label">Email</label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Input.Password" class="form-label"></label>
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
<label asp-for="Input.Password" class="form-label">Password</label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" asp-for="Input.RememberMe">
<label class="form-check-label" asp-for="Input.RememberMe">@Html.DisplayNameFor(m => m.Input.RememberMe)</label>
<div class="checkbox mb-3">
<label asp-for="Input.RememberMe" class="form-label">
<input class="form-check-input" asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>

<div>
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
<br />
<div>
<p>
<a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
Expand All @@ -45,6 +50,13 @@
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>
</div>

<hr />

<passkey-submit operation="@PasskeyOperation.Request" name="Input.Passkey" email-name="Input.Email" class="btn btn-outline-primary position-relative" formnovalidate>
Log in with a passkey
</passkey-submit>

</form>
</section>
</div>
Expand All @@ -57,8 +69,10 @@
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">
article
about setting up this ASP.NET application to support logging in via external services
</a>.
</p>
</div>
}
Expand All @@ -67,9 +81,9 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins)
@foreach (var provider in Model.ExternalLogins!)
{
<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>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>
Expand All @@ -81,6 +95,7 @@
</div>

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

Original file line number Diff line number Diff line change
Expand Up @@ -2,143 +2,142 @@
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using Fido2Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OpeniddictServer.Data;
using IdentityProvider.Data;
using System.ComponentModel.DataAnnotations;

namespace OpeniddictServer.Areas.Identity.Pages.Account
namespace IdentityProvider.Areas.Identity.Pages.Account;

[AllowAnonymous]
public class LoginModel : PageModel
{
public class LoginModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly Fido2Store _fido2Store;
private readonly ILogger<LoginModel> _logger;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;

public LoginModel(SignInManager<ApplicationUser> signInManager,
Fido2Store fido2Store,
ILogger<LoginModel> logger)
{
_signInManager = signInManager;
_fido2Store = fido2Store;
_logger = logger;
}
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger)
{
_signInManager = signInManager;
_logger = logger;
}

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IList<AuthenticationScheme> ExternalLogins { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string ReturnUrl { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string ErrorMessage { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }

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

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

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

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
ModelState.AddModelError(string.Empty, ErrorMessage);
}

public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");

// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

ReturnUrl = returnUrl;
}

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");

returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

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

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

ReturnUrl = returnUrl;
result = await _signInManager.PasskeySignInAsync(Input.Passkey.CredentialJson);
}
else if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
}

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
returnUrl ??= Url.Content("~/");

ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(Input.Email);
if (fido2ItemExistsForUser.Count > 0)
{
return RedirectToPage("./LoginFido2Mfa", new { ReturnUrl = returnUrl, Input.RememberMe });
}

return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}

// If we got this far, something failed, redisplay form
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
Expand Down
Loading
Loading