ASP.NET Core 7 Identity

Getting Started

The utilization of our NuGet package will streamline the integration process with ASP.NET Identity, facilitating a smoother and more efficient integration experience.

dotnet add package Passwordless.AspNetCore

Bootstrapping is now a straightforward process, where the addition of Passwordless to the IdentityBuilder becomes a requisite step.

The example below assumes your appsettings.json will add all configuration settings under the key Passwordless.

builder.Services
    .AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<PasswordlessContext>()
    .AddPasswordless(builder.Configuration.GetSection("Passwordless"));

At a minimum, you will be required to furnish the ApiKey (public key) and ApiSecret (private key).

{
  "Passwordless": {
    "ApiKey": "***:public:***",
    "ApiSecret": "***:secret:***",
    "Register": {
      "Discoverable": true
    }
  }
}

Now, we'll need to modify the WebApplication object to add the Passwordless routing to make registering, logging in and managing credentials easier.

The endpoints are documented in a later section on this page.

app.MapPasswordless(enableRegisterEndpoint: true);

Now we will create our registration page at /Account/Register. When we load the page, we will present a form to the user.

When we click the Register button, OnPostAsync will be called. When the form is valid, we will set the flag which will allow our Javascript code to run to allow registration of the token.

Calling POST /passwordless-register will create our IdentityUser and return a registration token in its response. We will then be able to use that token to create our passkeys.

@page
@using Passwordless.Net
@using Microsoft.Extensions.Options
@using Passwordless.AspNetCore
@model RegisterModel
@inject IOptions<PasswordlessAspNetCoreOptions> PasswordlessOptions;

@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

@{
    var canAddPasskeys = ViewData["CanAddPasskeys"] is true;
}

<div class="row">
    <div class="col-12">
        <form class="needs-validation" action="" method="POST">
            <div class="mb-3">
                <label asp-for="Form.Username" class="form-label">Username</label>
                <input
                        placeholder="Jane Doe"
                        type="text"
                        asp-for="Form.Username"
                        class="form-control"
                        id="username"/>
                <span class="text-danger" asp-validation-for="Form.Username"></span>
            </div>
            <div class="mb-3">
                <label asp-for="Form.Email" class="form-label">Email</label>
                <input
                        placeholder="janedoe@example.org"
                        type="text"
                        asp-for="Form.Email"
                        class="form-control"
                        id="email"/>
                <span class="text-danger" asp-validation-for="Form.Email"></span>
            </div>
            <div class="text-danger" asp-validation-summary="ModelOnly"></div>
            <div>
                <button type="submit" class="btn-primary">Register</button>
            </div>
        </form>
    </div>
</div>

@if (canAddPasskeys)
{
<script src="https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js"></script>
<script>
    async function register() {
        const username = document.getElementById('username').value;
        const email = document.getElementById('email').value;
        const registrationRequest = {
            email: email,
            username: username,
            displayName: username,
            aliases: [email]
        };

        const registrationResponse = await fetch('/passwordless-register', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(registrationRequest)
        });

        // If no error then deserialize and use returned token to create now our passkeys
        if (registrationResponse.ok) {
            const registrationResponseJson = await registrationResponse.json();
            const token = registrationResponseJson.token;

            // We need to use Client from https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js which is imported above.
            const p = new Passwordless.Client({
                apiKey: '@PasswordlessOptions.Value.ApiKey',
                apiUrl: '@PasswordlessOptions.Value.ApiUrl'
            });
            const registeredPasskeyResponse = await p.register(token, email);
        }
    }

    register();
</script>
}
public IActionResult OnGet()
{
    if (HttpContext.User.Identity is { IsAuthenticated: true })
    {
        return LocalRedirect("/");
    }

    return Page();
}

public async Task OnPostAsync(FormModel form, CancellationToken cancellationToken)
{
    if (!ModelState.IsValid) return;
    _logger.LogInformation("Registering user {username}", form.Username);
    ViewData["CanAddPasskeys"] = true;
}

public FormModel Form { get; init; } = new();

Regarding the login page, /Account/Login, clicking the 'Login' button will once again trigger the setting of a similar flag, enabling the execution of our JavaScript code.

There are two approaches you can take. You can either request an "alias" to verify which passkeys correspond to the alias, or if you have discoverable credentials, an input form may not be necessary.

The process of logging in differs from registration. During login, you will request valid passkeys from your authenticator to obtain an authentication token. Subsequently, this authentication token will be transmitted to your backend to initiate the session.

With the "Passwordless ASP.NET Identity SDK," you can streamline this process by making a simple call to POST /passwordless-login, and the SDK will handle all the necessary steps for you.

Upon a successful authentication, our sample application will automatically redirect you to the /Authorized/HelloWorld page, which requires you to be logged in to access.

@page
@model LoginModel
@using Microsoft.Extensions.Options
@using Passwordless.AspNetCore
@inject IOptions<PasswordlessAspNetCoreOptions> PasswordlessOptions;
@{
    ViewData["Title"] = "Login";
}

<h1>@ViewData["Title"]</h1>

@{
    var canLogin = ViewData["CanLogin"] != null && (bool)ViewData["CanLogin"];
}

<div class="row">
    <div class="col-12">
        <form class="needs-validation" action="" method="POST">
            <div class="mb-3">
                <label asp-for="Form.Email" class="form-label">Email</label>
                <input
                        placeholder="janedoe@example.org"
                        type="text"
                        asp-for="Form.Email"
                        class="form-control"
                        id="email"/>
                <span class="text-danger" asp-validation-for="Form.Email"></span>
            </div>
            <div class="text-danger" asp-validation-summary="ModelOnly"></div>
            <div>
                <button type="submit" class="btn-primary">Login</button>
            </div>
        </form>
    </div>
</div>

@if (canLogin)
{
<script src="https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js"></script>
<script>
    async function login() {
        const alias = document.getElementById('email').value;
        const p = new Passwordless.Client({
            apiKey: '@PasswordlessOptions.Value.ApiKey',
            apiUrl: '@PasswordlessOptions.Value.ApiUrl'
        });
        const loginPasskeyResponse = await p.signinWithAlias(alias);
        if (!loginPasskeyResponse) {
            return;
        }
        const loginRequest = {
            token: loginPasskeyResponse.token
        };
        const loginResponse = await fetch('/passwordless-login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(loginRequest)
        });

        if (loginResponse.ok) {
            console.log('login successful: ' + (await loginResponse.text()));

            // Redirect to authorized page /Authorized/HelloWorld
            window.location.href = '/Authorized/HelloWorld';
        }
    }

    login();
</script>
}

If we visit the login page when we're already authenticated, we do want to redirect elsewhere.

public IActionResult OnGet()
{
    if (HttpContext.User.Identity is { IsAuthenticated: true })
    {
        return LocalRedirect("/");
    }
    return Page();
}

public async Task OnPostAsync(LoginForm form, CancellationToken cancellationToken)
{
    if (!ModelState.IsValid) return;
    _logger.LogInformation("Logging in user {email}", form.Email);
    ViewData["CanLogin"] = true;
}

public LoginForm Form { get; } = new();

You can then access information about your logged in user from the HttpContext:

public class HelloWorldModel : PageModel
{
    private readonly ILogger<HelloWorldModel> _logger;

    public HelloWorldModel(ILogger<HelloWorldModel> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public void OnGet()
    {
        var identity = HttpContext.User.Identity ;
        var email = User.FindFirstValue(ClaimTypes.Email);
        AuthenticatedUser = new AuthenticatedUserModel(identity.Name, email);
    }

    public AuthenticatedUserModel AuthenticatedUser { get; private set; }
}

public record AuthenticatedUserModel(string Username, string Email);

Configuration

KeyDefaultDescription
ApiKeyYour public API key
ApiSecretYour private API key
ApiUrlhttps://v4.passwordless.devWhere your Passwordless.dev instance is running
SignInSchemeIdentity.ApplicationControls the scheme that will be used to handle the sign in
UserIdClaimTypeControls the claim type that will be used to find the user id from an authenticated users, see ClaimsPrincipal. If it is null it will fallback to ClaimsIdentityOptions.UserIdClaimType from IdentityOptions.ClaimsIdentity and if that is null will fallback to ClaimTypes.NameIdentifier.
Register__AliasHashingtrue[false/true] When set to true, aliases are only stored in their hashed form.
Register__AttestationNone[None/Direct/Indirect] A value of None indicates that the server does not care about attestation. A value of Indirect means that the server will allow for anonymized attestation data. Direct means that the server wishes to receive the attestation data from the authenticator.
Register__AuthenticationTypeany
Register__Expiration"00:02:00"["hh:mm:ss"]
Register__Discoverabletrue[false/true] Discoverable Credentials store private keys and associated data in the authenticator's memory instead of on a server. This eliminates the need for the server to send the credential to the authenticator for decryption, reducing the reliance on usernames and passwords for identity verification.
Register__UserVerificationPreferred[Discouraged/Preferred/Required] A WebAuthn Relying Party may require user verification for some of its operations but not for others, and may use this type to express its needs.

Advanced

If you find yourself requiring greater flexibility, we invite you to explore our JavaScript client library and .NET SDK. This option is particularly suitable if you seek more granular control over the ASP.NET Identityopen in new window framework or aspire to undertake a fully customized implementation.