Moderne applikationer bevæger sig i stigende grad væk fra traditionelle adgangskoder og over mod adgangskodefri autentificeringsmetoder, der både kan styrke sikkerheden og gøre login lettere for brugerne. I denne artikel viser jeg, hvordan brugere autentificeres i en ASP.NET Core applikation med FoxIDs og OpenID Connect, så de kan logge ind helt uden adgangskode med engangskoder sendt via e-mail.

Vi begynder med et tomt FoxIDs miljø, opretter en ny ASP.NET Core webapplikation, forbinder den med OpenID Connect og aktiverer til sidst adgangskodefrit login med e-mailbaseret OTP.

Opret et FoxIDs udviklingsmiljø

Jeg begyndte med at oprette et nyt, tomt udviklingsmiljø i FoxIDs. Et tomt miljø gør det overskueligt at konfigurere autentifikation fra bunden og gentage trinnene i jeres egen tenant.

Tomt FoxIDs miljø

Hvis I ikke har brugt FoxIDs før, kan I oprette en gratis tenant her:

https://www.foxids.com

Hver tenant indeholder to miljøer, der er klar til brug, så I frit kan afprøve opsætningen.

Konfigurér autentifikation

Begynd med at konfigurere, hvordan brugerne skal logge ind. Her skal brugerne logge ind uden adgangskode og kun bruge deres e-mailadresse og et engangskodeord (OTP), som sendes til dem.

  1. Vælg fanen Authentications i FoxIDs miljøet.
  2. Vælg Default — User Login UI.
  3. Deaktivér Password authentication under Authentication, og aktivér Passwordless with email (one-time password).
  4. Vælg Update for at gemme ændringerne.
Konfiguration af loginmetoden

Brugerne kan nu oprette en ny konto og logge ind uden adgangskode ved blot at indtaste deres e-mailadresse og den OTP, de modtager.

Opret ASP.NET Core applikationen

Nu skal vi bruge en applikation, som skal autentificere brugere. I kan bruge Visual Studio, VS Code eller et andet værktøj, der understøtter almindelig ASP.NET Core udvikling.

Læs artiklen “Adgangskodefri autentifikation i en AI-genereret ASP.NET Core app med FoxIDs og e-mail OTP”, hvis I vil generere ASP.NET Core applikationen med en AI-prompt.

I denne vejledning opretter jeg en enkel ASP.NET Core Razor Pages applikation til .NET 10 i Visual Studio. Den samme fremgangsmåde kan bruges med MVC, en Blazor Web App eller et andet ASP.NET Core projekt.

Opret ASP.NET standardapplikation

Kør applikationen. Eksempelapplikationen kører lokalt på https://localhost:7283/.

Kør ASP.NET standardapplikationen

Eksempelkoden findes på GitHub.

Konfigurér webapplikationen i FoxIDs

Nu kender vi webapplikationens lokale adresse og kan registrere den i FoxIDs, så den kan autentificere brugere gennem OpenID Connect.

  1. Vælg fanen Applications i FoxIDs miljøet.
  2. Vælg New application og derefter Web application — OpenID Connect.
  3. Angiv et navn under Name, eksempelvis ASP.NET web application.
  4. Tilføj applikationens basisadresse under Redirect URI: https://localhost:7283/ FoxIDs tillader som standard omdirigering til efterfølgende sider under denne basisadresse. I kan kræve absolutte redirect URI'er, hvis valideringen skal være mere restriktiv.
  5. Vælg Create.
Tilføj applikation i FoxIDs

FoxIDs viser nu forbindelsesoplysningerne til den nye applikation, herunder:

  • Authority
  • Client ID
  • Client Secret
Applikation oprettet i FoxIDs

Kopiér Authority og Client ID til projektets IdentitySettings sektion. Gem Client Secret lokalt i .NET user-secrets eller i en sikker miljøvariabel eller secret store i produktion.

Konfigurér autentifikation i ASP.NET applikationen

Når FoxIDs applikationen er oprettet, skal autentifikation konfigureres i ASP.NET Core projektet. Vi bruger cookie authentication til den lokale session og OpenID Connect til kommunikationen med FoxIDs.

Åbn Program.cs, og tilføj autentifikationskonfigurationen med builder.Services.AddAuthentication... og 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();

Tilføj de ikke-fortrolige FoxIDs forbindelsesoplysninger i appsettings.json. Hold client secret ude af versionsstyringen, og indlæs den fra .NET user-secrets under lokal udvikling eller fra en sikker miljøvariabel eller secret store i produktion.

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

Gem client secret lokalt uden at skrive den i appsettings.json:

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

Erstat eksempelværdierne med Authority, Client ID og Client Secret fra jeres eget FoxIDs miljø. Den rigtige secret må aldrig committes.

Vis claims på forsiden

I dette eksempel vises claims på forsiden. Åbn Index.cshtml, og erstat indholdet:

@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>
}

Tilføj en loginmenu i applikationen

For at brugerne kan logge ind, tilføjer vi en partial view med login- og logoutfunktioner.

Opret filen Pages/Shared/_LoginPartial.cshtml med følgende indhold:

@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>
}

Medtag derefter login partial i navigationsmenuen. Tilføj partial-elementet sidst i navigationslinjen i Pages/Shared/_Layout.cshtml:

<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>

Tilføj derefter endpoints til login- og logoutanmodninger. Opret mappen Controllers og controllerfilen Controllers/AuthController.cs med følgende indhold:

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);
        }
    }
}

Kør og genindlæs applikationen. Menupunktet Log in vises nu i topmenuen.

Eksempelapplikation

Log ind

Vælg Log in. I bliver omdirigeret til FoxIDs loginskærmen.

Vælg Create user.

FoxIDs loginskærm

Udfyld formularen Create user med jeres e-mailadresse eller en testadresse, I har adgang til, samt fornavn og efternavn. Vælg derefter Create.

Opret en testbruger

Indtast det engangskodeord (OTP), I har modtaget via e-mail, og vælg Log in.

Login med engangskodeord (OTP)

I er nu logget ind, og testbrugerens claims vises.

Brugeren er logget ind, og claims vises

Prøv til sidst at logge ud og ind igen for at gennemføre det adgangskodefri loginflow med e-mail OTP.