Modern applications increasingly move away from traditional passwords and toward passwordless authentication methods that are both more secure and easier for users. In this post, I’ll show how to authenticate users in an ASP.NET Core application using FoxIDs with OpenID Connect and enable a fully passwordless sign-in experience using one-time codes delivered via email.

We’ll start from a clean FoxIDs environment, create a new ASP.NET Core web app, connect it with OpenID Connect, and finally enable passwordless login with email-based OTP.

Creating a FoxIDs Dev Environment

I began by creating a new empty dev environment in FoxIDs. A clean environment makes it simple to set up authentication from scratch and ensures you can replicate these steps in your own tenant.

Empty FoxIDs environment

If you haven’t used FoxIDs before, you can create a free tenant at:

https://www.foxids.com

Each tenant comes with two ready-to-use environments so you can experiment freely.

Configure Authentication

Start by configuring how users should sign in. In this case, we want users to log in without a password, using only their email address and a one-time password (OTP) sent to them.

  1. Select the Authentications tab in your FoxIDs environment.
  2. Click Default — User Login UI.
  3. In the Authentication section, disable Password authentication and enable Passwordless with email (one-time password).
  4. Click Update to save the changes.
Configure authentication login method

This enables users to create a new account and provides a passwordless experience where they authenticate simply by entering their email address and the OTP they receive.

Creating the ASP.NET Core Application

Next, we need an application to authenticate. You can use either Visual Studio or VS Code — anything that supports standard ASP.NET Core development.

Read the “Passwordless Authentication in an AI-generated ASP.NET Core App with FoxIDs and Email-Based OTP” post if you want to generate the ASP.NET Core application with an AI prompt.

For this walkthrough, I’m creating a simple ASP.NET Core Razor Pages application targeting .NET 10 in Visual Studio, but the same approach works for MVC, a Blazor Web App, or any other ASP.NET Core project.

Create default ASP.NET app

Run the app; the sample app runs locally on https://localhost:7283/

Run default ASP.NET app

You can find the sample code in: https://github.com/ITfoxtec/dotnet.samples/tree/main/WebAppPasswordLessEmail

Configure the web application in FoxIDs

Now that we know the web application’s local address, we can register it in FoxIDs so it can authenticate users through OpenID Connect.

  1. Select the Applications tab in your FoxIDs environment.
  2. Choose New application, then select Web application — OpenID Connect.
  3. In the Name, enter a name, for example: ASP.NET web application.
  4. In the Redirect URI, add the application’s base address: https://localhost:7283/ By default, FoxIDs allows redirecting to any subsequent page under this base URL. If you prefer stricter validation, you can require absolute redirect URIs.
  5. Click Create.
Add app in FoxIDs

FoxIDs now displays the connection details for your new application, including:

  • Authority
  • Client ID
  • Client Secret
App created in FoxIDs

Copy the Authority and Client ID into the project’s IdentitySettings section. Store the Client Secret in .NET user-secrets locally or in a secure environment variable or secret store in production.

Configure authentication in the ASP.NET application

With the FoxIDs application created, the next step is to configure authentication in the ASP.NET Core project. We will use cookie authentication for the local session and OpenID Connect to communicate with FoxIDs.

Open Program.cs and add the authentication configuration with (builder.Services.AddAuthentication... and app.UseAuthentication()😉:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Logging;

var builder = WebApplication.CreateBuilder(args);

// Detailed authentication errors can help locally, but must not be enabled in production.
if (builder.Environment.IsDevelopment())
{
    IdentityModelEventSource.ShowPII = true;
}

// Add authentication with cookie and OpenID Connect
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.Authority = builder.Configuration["IdentitySettings:Authority"];
        options.ClientId = builder.Configuration["IdentitySettings:ClientId"];
        options.ClientSecret = builder.Configuration["IdentitySettings:ClientSecret"];
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.SaveTokens = true;

        options.Scope.Add("email");
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");

        options.MapInboundClaims = false;
        options.TokenValidationParameters.NameClaimType = "sub";
        options.TokenValidationParameters.RoleClaimType = "role";

        options.Events = new OpenIdConnectEvents
        {
            OnTokenValidated = async context =>
            {
                // Custom claims transformation or other logic can be added here.
                await Task.CompletedTask;
            },
            OnAuthenticationFailed = context =>
            {
                if (!builder.Environment.IsDevelopment())
                {
                    return Task.CompletedTask;
                }

                context.HandleResponse();
                context.Response.StatusCode = 500;
                context.Response.ContentType = "text/plain";
                return context.Response.WriteAsync(context.Exception.ToString());
            }
        };
    });

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapStaticAssets();
app.MapControllers();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();

Add the non-secret FoxIDs connection details to appsettings.json. Keep the client secret out of source control and load it from .NET user secrets during local development or from a secure environment variable or secret store in production.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "IdentitySettings": {
    "Authority": "https://foxids.com/{tenant}/{environment}/{application}/",
    "ClientId": "your-client-id"
  }
}

Store the client secret locally without writing it to appsettings.json:

dotnet user-secrets init
dotnet user-secrets set "IdentitySettings:ClientSecret" "your-client-secret"

Replace the example values with the Authority, Client ID, and Client Secret from your own FoxIDs environment. Never commit the real secret.

Show claims on the main page

For the purpose of this sample show claims on the main page. Open Index.cshtml change the content:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@if (User.Identity?.IsAuthenticated == true)
{
    <h2 class="mt-4">Claims</h2>
    <table class="table table-striped table-bordered">
        <thead>
            <tr>
                <th>Type</th>
                <th>Value</th>
            </tr>
        </thead>
        <tbody>
        @foreach (var claim in User.Claims)
        {
            <tr>
                <td>@claim.Type</td>
                <td>@claim.Value</td>
            </tr>
        }
        </tbody>
    </table>
}
else
{
    <p class="mt-4 text-muted">Not signed in.</p>
}

Add a login menu to the application

To let users sign in, we add a partial view for login and logout controls.

Create a new file:Pages/Shared/_LoginPartial.cshtml with the content:

@if (User.Identity?.IsAuthenticated == true)
{
    <ul class="navbar-nav">
        <li class="nav-item">
            <form class="form-inline" asp-controller="Auth" asp-action="Logout" method="post">
                @Html.AntiForgeryToken()
                <button type="submit" class="nav-link btn btn-link text-dark">Log off</button>
            </form>
        </li>
    </ul>
}
else
{
    <ul class="navbar-nav navbar-right">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-controller="Auth" asp-action="Login">Log in</a>
        </li>
    </ul>
}

Then include the login partial in the navigation menu. In Pages/Shared/_Layout.cshtml add the partial login element to the end of the navbar:

<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>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
        </li>
    </ul>
    <partial name="_LoginPartial" />
</div>

Then add a landing page for the login and logout requests. Create a Controllers folder and create a new controller file in the folder: Controllers/AuthController.cs with the content:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace WebAppPasswordLessEmail.Controllers
{
    [AllowAnonymous]
    [Route("[controller]/[action]")]
    public class AuthController : Controller
    {
        [HttpGet]
        public IActionResult Login()
        {
            var redirectUri = Url.Content("~/");
            if (User.Identity?.IsAuthenticated == true)
            {
                return LocalRedirect(redirectUri);
            }

            return Challenge(new AuthenticationProperties { RedirectUri = redirectUri },
                OpenIdConnectDefaults.AuthenticationScheme);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Logout()
        {
            var redirectUri = Url.Content("~/");
            if (User.Identity?.IsAuthenticated != true)
            {
                return LocalRedirect(redirectUri);
            }

            return SignOut(new AuthenticationProperties { RedirectUri = redirectUri },
                CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
        }
    }
}

Run and reload the application and you will now see a Log in option in the top menu.

Sample application

Log in

Click Log in and you are redirected to the FoxIDs login screen.

Select Create user.

FoxIDs login screen

Fill out the Create user form with your email (or a test email you have access to), given name, and family name, then click Create.

Create a test user

Enter the one-time password (OTP) you received by email, then click Log in.

Login with one-time password (OTP)

You are logged in and your test user’s claims are displayed.

Logged in, show claims

Next, try logging off and signing in again to experience the passwordless login flow using email OTP.