Get your free and exclusive +30-page Authentication Analytics Whitepaper

Building a Passkey Login Page with ASP.NET Core

This tutorial shows C# / .NET developers how to integrate passkeys into an ASP.NET Core app. Besides passkeys, simple session management is also implemented.

Blog-Post-Author

Nicolai

Created: October 17, 2023

Updated: March 25, 2026

passkeys asp net core
PasskeysCheatsheet Icon

Looking for a dev-focused passkey reference? Download our Passkeys Cheat Sheet. Trusted by dev teams at Ally, Stanford CS & more.

Get Cheat Sheet
Key Facts
  • The <corbado-auth> web component handles all passkey authentication in ASP.NET Core Razor Pages, eliminating manual WebAuthn ceremony implementation from application code.
  • Session validity is confirmed by reading the cbo_short_session cookie, a JWT whose RSA signature is validated against Corbado's JWKS public key endpoint.
  • The Relying Party ID must be the bare domain only (no protocol, no port and no path) where passkeys are bound, for example: localhost.
  • User profile data is extracted from JWT claims after validation, providing userID, userName and userEmail directly from the short-term session token.
  • Corbado's Redirect URL setting determines which page receives the session cookie after successful passkey authentication and must be registered in the developer panel.

1. Introduction#

In this blog post, we help C# / .NET developers to implement a sample application with passkey authentication using ASP.NET Core. To make passkeys work, we use Corbado's passkey-first web component that automatically connects to a passkeys backend.

If you want to see the finished code, please have a look at our sample application GitHub repository.

The result looks as follows:

2. ASP.NET Core passkey project prerequisites#

This tutorial assumes basic familiarity with C#, HTML, and the ASP.NET Core framework. Let's dive in!

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

3. Repository structure for the ASP.NET Core passkey project#

An ASP.NET Core project contains many files, but the important ones are the following:

├── corbado-demo | ├── api | | ... | | └── Pages | | ├── Index.cshtml # The page which shows info about your profile when logged in | | └── Login.cshtml # The login page | | | └── Properties | └── launchSettings.json # Contains environment variables

4. Set up your Corbado account and project#

Visit the Corbado developer panel to sign up and create your account (youll see a passkey sign-up in action here!).

In the appearing project wizard, select Web app as type of app and afterwards select No existing users, as were building a new app from scratch. Moreover, providing some details regarding your frontend and backend tech stack as well as the main goal you want to achieve with Corbado helps us to customize and smoothen your developer experience.

Next, we navigate to Settings > General > URLs and set the Application URL, Redirect URL and Relying Party ID to the following values (we will host our app on port 7283):

  • Application URL: Provide the URL where you embedded the web component, here: http://localhost:7283
  • Redirect URL: Provide the URL your app should redirect to after successful authentication and which gets sent a short-term session cookie, here: http://localhost:7283/
  • Relying Party ID: Provide the domain (no protocol, no port and no path) where passkeys should be bound to, here: localhost
Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

5. Create ASP.NET Core app#

To initialize our project, we open Visual Studio and use the wizard to create an ASP.NET Core web application:

Select as seen above and click Continue.

Select the target framework and use No Authentication as authentication option.

Finally, give your project a name and a location and click Create. Visual Studio will automatically open the project for you.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.

Join Passkeys Community

In the pages folder, we can delete all files except for "_ViewImports.cshtml" and "_ViewStart.cshtml":

Then, we adjust the navigation in Pages/Shared/Layout.cshtml (We already added the login and index routes here):

@{ string projectID = Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID"); string authUrl = "https://" + @projectID + ".frontendapi.corbado.io/auth.js"; string utilityUrl = "https://" + @projectID + ".frontendapi.corbado.io/utility.js"; string cssUrl = "https://" + @projectID + ".frontendapi.corbado.io/auth_cui.css"; } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - corbado_demo</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/corbado_demo.styles.css" asp-append-version="true" /> <link rel="stylesheet" href=@cssUrl asp-append-version="true" /> <script src=@authUrl></script> <script src=@utilityUrl></script> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <div class="container"> <a class="navbar-brand" asp-area="" asp-page="/Index">corbado_demo</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> </ul> <partial name="_LoginPartial" /> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> @RenderBody() </main> </div> <footer class="border-top footer text-muted"> <div class="container"> &copy; 2023 - corbado_demo </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>

5.1 Configure environment variables#

We will need the Corbado project ID in the next steps, so well add it as an environment variable. For this, we edit the /Properties/launchSettings.json file and paste our Corbado project ID:

{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:7887", "sslPort": 44317 } }, "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:5286", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "CORBADO_PROJECT_ID": "pro-xxx" }, "dotnetRunMessages": true }, "https": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:7283;http://localhost:5286", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "CORBADO_PROJECT_ID": "pro-xxx" }, "dotnetRunMessages": true }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "CORBADO_PROJECT_ID": "pro-xxx" } } } }

5.2 Install NuGet packages#

Make sure you have all of the following NuGet packages installed ("Tools > Manage NuGetPackages" inside Visual Studio).

6. Create passkey login page#

Right click the /Pages folder and select New file. Then select ASP.NET Core > RazorPage (with page model) and click Create.

This will create a Login.cshtml file as well as a Login.cshtml.cs file.

Paste the following code into the Login.cshtml file:

@page @model LoginModel @{ ViewData["Title"] = "Login"; } <div class="text-center"> @if (Model.ShowRequestId) { <p> <strong>Request ID:</strong> <code>@Model.RequestId</code> </p> } <div> <p>Project ID: @Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID")</p> <corbado-auth project-id=@Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID") conditional="yes"> <input name="username" id="corbado-username" required autocomplete="webauthn" /> </corbado-auth> </div> </div>

Afterwards, add this code to the Login.cshtml.cs file:

@page @model IndexModel @{ ViewData["Title"] = "Login"; } <div class="text-center"> @if (@Model.ShowRequestId) { <p> <strong>Request ID:</strong> <code>@Model.RequestId</code> </p> } <div> @if (@Model.userID != null) { <h1 class="display-4">Welcome</h1> <p>Project ID: @Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID")</p> <p>User ID: @Model.userID</p> <p>User Name: @Model.userName</p> <p>User Email: @Model.userEmail</p> <button id="logoutButton">Logout</button> <script inline="javascript"> const projectID = "@Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID")"; const session = new Corbado.Session(projectID); const logoutButton = document.getElementById('logoutButton'); logoutButton.addEventListener('click', function() { session.logout() .then(() => { window.location.replace("/"); }) .catch(err => { console.error(err); }); }); </script> } else { <p>Please login to see your profile</p> } </div> </div>

Our login page contains the Corbado web component, which will handle authentication.

7. Add index page#

Like in step 6, we create a Razor page with page model and call it Index which will result in Index.cshtml and Index.cshtml.cs being created. After successful authentication, the Corbado web component redirects the user to the provided Redirect URL (http://localhost:7283/). This is the index page we just created. If someone is logged in, it shows information about the user and provides a button to log out. If no user is available, the page just displays an invitation to login.

For this, we add the following code to Index.cshtml:

using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace corbado_demo.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [IgnoreAntiforgeryToken] public class LoginModel : PageModel { public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); private readonly ILogger<LoginModel> _logger; public LoginModel(ILogger<LoginModel> logger) { _logger = logger; } public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } }

7.1 Verify session#

Before we can use information embedded in the session, we need to verify that the session is valid. We therefore take the cbo_short_session cookie (the session) and verify its signature using the public key from Corbado. This happens in the Index.cshtml.cs file. We also verify that the issuer is correct (remember to use your own project ID):

using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using Newtonsoft.Json.Linq; using System.Security.Cryptography; using System.Diagnostics; namespace corbado_demo.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [IgnoreAntiforgeryToken] public class IndexModel : PageModel { public string? RequestId { get; set; } public string? userID { get; set; } public string? userName { get; set; } public string? userEmail { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); private readonly ILogger<IndexModel> _logger; private readonly IHttpContextAccessor _httpContextAccessor; public IndexModel(ILogger<IndexModel> logger, IHttpContextAccessor httpContextAccessor) { _logger = logger; _httpContextAccessor = httpContextAccessor; } public async Task OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; try { string projectID = Environment.GetEnvironmentVariable("CORBADO_PROJECT_ID"); string issuer = $"https://{projectID}.frontendapi.corbado.io"; string jwksUri = $"https://{projectID}.frontendapi.corbado.io/.well-known/jwks"; using (var httpClient = new HttpClient()) { var response = await httpClient.GetStringAsync(jwksUri); var json = JObject.Parse(response); var publicKey = json["keys"][0]["n"].ToString(); var publicKeyBase64 = Base64UrlToBase64(publicKey); var rsaParameters = new RSAParameters { Exponent = Convert.FromBase64String(json["keys"][0]["e"].ToString()), Modulus = Convert.FromBase64String(publicKeyBase64) }; var token = _httpContextAccessor.HttpContext.Request.Cookies["cbo_short_session"]; var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = false, ValidateLifetime = true, IssuerSigningKey = new RsaSecurityKey(rsaParameters), ClockSkew = TimeSpan.Zero, RequireSignedTokens = true, RequireExpirationTime = true, ValidateIssuerSigningKey = true }; try { var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _); } catch (SecurityTokenValidationException ex) { Console.WriteLine(ex.Message); } } } catch (Exception ex) { Console.WriteLine(ex.Message); } } // Helper function to convert Base64Url to Base64 private string Base64UrlToBase64(string base64Url) { base64Url = base64Url.Replace('-', '+').Replace('_', '/'); while (base64Url.Length % 4 != 0) { base64Url += '='; } return base64Url; } }

7.2 Get data from Corbado session#

Finally, we can extract the information stored in the JWT claims:

var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _); userID = claimsPrincipal.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value; userName = claimsPrincipal.FindFirst("name")?.Value; userEmail = claimsPrincipal.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;

The variables "userID", "userName" and "userEmail" are used in the Index.cshtml template from step 7.

8. Start using passkeys with our ASP.NET Core implementation#

To start our application, we head into Visual Studio and run the project (Hit the play button or go to Debug -> Start Debugging).

When visiting http://localhost:7283 you should see the following screen:

Clicking on Login will lead you to the Login page:

After successful sign up / login, you see the following info on the Index page:

9. Conclusion#

This tutorial showed how easy it is to add passwordless authentication with passkeys to an ASP.NET Core app using Corbado. Besides the passkey-first authentication, Corbado provides simple session management, that we used for a retrieval of basic user data. If you want to read more about how Corbado's session management please check the docs here. If you want to add Corbado to your existing app with existing users, please see our documentation here.

Frequently Asked Questions#

How do I verify a passkey session token in an ASP.NET Core application?#

Retrieve the cbo_short_session cookie from the HTTP request, then fetch the RSA public key from Corbado's JWKS endpoint at https://{projectID}.frontendapi.corbado.io/.well-known/jwks. Use the .NET JwtSecurityTokenHandler with TokenValidationParameters to enforce issuer validation, signature verification and expiry checks before trusting any session data.

How do I extract the logged-in user's details from a Corbado passkey session in .NET?#

After validating the JWT with JwtSecurityTokenHandler, call claimsPrincipal.FindFirst() with the appropriate claim URIs to read user data. The validated token exposes userID, userName and userEmail as standard JWT claims without any additional API call.

What is the difference between Application URL, Redirect URL and Relying Party ID when configuring a Corbado passkey project?#

The Application URL is the page where the <corbado-auth> web component is embedded. The Redirect URL is where the app sends the user and the short-term session cookie after successful authentication. The Relying Party ID is the bare domain (no protocol, no port and no path) to which the passkeys are cryptographically bound.

How do I add the Corbado passkey web component scripts to an ASP.NET Core layout?#

Construct the script URLs dynamically from your Corbado project ID and load auth.js, utility.js and auth_cui.css from https://{projectID}.frontendapi.corbado.io/ inside the shared _Layout.cshtml file. The project ID is stored as an environment variable (CORBADO_PROJECT_ID) in launchSettings.json and referenced at runtime with Environment.GetEnvironmentVariable.

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook