Anmeldung zu einer ASP.NET-Core-App hinzufügen
Diese Schnellstart-Anleitung fügt eine KeenAuth-Anmeldung zu einer ASP.NET Core 10 Razor-Pages-App hinzu, mit Microsoft.AspNetCore.Authentication.OpenIdConnect, dem Standard-OpenID-Connect-Client von ASP.NET Core. KeenAuth hat noch kein .NET SDK, und es wird keines benötigt: KeenAuth ist ein Standard-OpenID-Connect-Provider. Ihre App ist der OAuth-Client und behält alle Tokens auf dem Server; der Browser erhält nur ein Cookie, das einen zufälligen Sitzungsschlüssel enthält. Sie konfigurieren die Anmeldung, schützen eine Seite, rufen eine geschützte API mit dem Access Token des Benutzers auf, melden sich ab und testen die App offline.
Die App hat diese Routen:
| Route | Funktion |
|---|---|
GET / |
Öffentliche Startseite: ein Anmeldelink oder der Name des Benutzers und eine Schaltfläche zum Abmelden |
GET /login |
Startet die Anmeldung: leitet zu KeenAuth weiter |
GET /signin-oidc |
Wird vom OpenID-Connect-Handler bereitgestellt: der Callback |
GET /dashboard |
Erfordert eine Sitzung. Zeigt den Benutzer und die Nachrichten, die Ihre API zurückgibt |
POST /logout |
Beendet die Sitzung hier und bei KeenAuth. Benötigt das Antiforgery-Token |
Bevor Sie beginnen
- Eine KeenAuth-Umgebung. Ihr Issuer sieht etwa so aus:
https://your-environment.eu.keenauth.app. - Eine Webanwendung in der Umgebung (Anwendung anlegen) mit einem Client Secret. Der Handler sendet das Secret im Body der Token-Anfrage, wählen Sie daher
client_secret_post. Registrieren Sie in einerdevelopment-Umgebung die Redirect-URIhttp://localhost/signin-oidcund die Post-Logout-Redirect-URIhttp://localhost/; eine Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa8080. - Eine API, die in der Umgebung registriert ist (API anlegen) mit dem Scope
messages:read. Ihridentifier, zum Beispielhttps://api.example.com, ist die Audience ihrer Access Tokens. Der Express-Schnellstart erstellt eine API, die diese Tokens akzeptiert. - Das .NET 10 SDK (10.0.403 oder ein späterer Patch).
1. Paket hinzufügen
Die App hat ein Paket, das auf eine exakte Version fixiert ist. Die Lock-Datei (packages.lock.json, aktiviert mit RestorePackagesWithLockFile) fixiert jedes andere Paket mit seinem Inhalts-Hash.
<!-- The standard OpenID Connect client of ASP.NET Core (MIT). Exact version; the lock file pins the rest. -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="[10.0.14]" />
Stellen Sie im Locked Mode wieder her, damit eine Änderung am Abhängigkeitsgraphen fehlschlägt, statt stillschweigend durchzulaufen:
dotnet restore --locked-mode
2. Konfigurieren
Die App liest ihre Konfiguration nur aus Umgebungsvariablen. Committen Sie deren Werte niemals.
| Variable | Pflicht | Bedeutung |
|---|---|---|
KEENAUTH_ISSUER |
ja | Der Issuer Ihrer Umgebung, https |
KEENAUTH_CLIENT_ID |
ja | Die client_id der Webanwendung |
KEENAUTH_CLIENT_SECRET |
ja | Ihr Client Secret |
APP_BASE_URL |
ja | Die öffentliche URL der App. Die Redirect-URI wird daraus gebildet. http nur für localhost, 127.0.0.1 oder [::1] |
API_URL |
ja | Die API, die die App aufruft, zum Beispiel http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
ja | Der Identifier dieser API in KeenAuth |
SESSION_SECURE_COOKIE |
nein | false nur für einfaches http://localhost |
KeenAuthSettings.From liest diese Werte beim Start ein und prüft sie. Ein fehlender oder unbrauchbarer Wert stoppt die App, und keine Fehlermeldung enthält einen Wert.
public static KeenAuthSettings From(Func<string, string?> env) => new(
Url(Need(env, "KEENAUTH_ISSUER"), "KEENAUTH_ISSUER", httpsOnly: true, anyScheme: false),
Need(env, "KEENAUTH_CLIENT_ID"),
Need(env, "KEENAUTH_CLIENT_SECRET"),
Url(Need(env, "APP_BASE_URL"), "APP_BASE_URL", httpsOnly: false, anyScheme: false),
Url(Need(env, "API_URL"), "API_URL", httpsOnly: false, anyScheme: false),
// RFC 8707: the access token is requested for your API, which then accepts it as its audience.
Url(Need(env, "KEENAUTH_API_AUDIENCE"), "KEENAUTH_API_AUDIENCE", httpsOnly: false, anyScheme: true),
// The session cookie is Secure unless this is explicitly "false" (plain http://localhost).
!string.Equals(env("SESSION_SECURE_COOKIE"), "false", StringComparison.OrdinalIgnoreCase));
3. Sitzung auf dem Server halten
Das Cookie-Schema meldet den Benutzer bei Ihrer App an. Das Cookie enthält nur einen zufälligen Schlüssel; Benutzer und Tokens bleiben in einem Ticket-Speicher auf dem Server (MemoryTicketStore, im Speicher eines Prozesses).
.AddCookie(options =>
{
// The cookie holds only a random key; the user and the tokens stay in MemoryTicketStore.
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = settings.SecureCookie ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = false;
})
4. OpenID-Connect-Handler konfigurieren
Der Handler liest Discovery (<issuer>/.well-known/openid-configuration) und die Signaturschlüssel Ihrer Umgebung. Er verwendet den Authorization-Code-Flow mit PKCE (S256), state und nonce und prüft sie beim Rücksprung. Nennt Discovery einen Endpunkt für Pushed Authorization Requests, verwendet der Handler ihn (RFC 9126).
// The endpoints and the signing keys come from discovery: <issuer>/.well-known/openid-configuration.
options.Authority = settings.Issuer;
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientSecret;
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = OpenIdConnectResponseMode.Query;
// PKCE (RFC 7636) with S256; the handler also sends state and nonce and checks them on the way back.
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
// messages:read is a scope of your API; the example API requires it on /api/messages.
options.Scope.Add("messages:read");
options.SaveTokens = true;
options.MapInboundClaims = false;
options.GetClaimsFromUserInfoEndpoint = false;
// Pushed authorization requests (RFC 9126) when the environment offers the endpoint.
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;
options.CallbackPath = "/signin-oidc";
// ID tokens must be signed, with the keys of the environment and one of these algorithms.
options.TokenValidationParameters.RequireSignedTokens = true;
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
options.TokenValidationParameters.ValidAlgorithms = ["RS256", "ES256", "PS256"];
options.TokenValidationParameters.NameClaimType = "name";
// These cookies carry the state and the nonce between the redirect and the callback.
var cookiePolicy = settings.SecureCookie ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
options.NonceCookie.SameSite = SameSiteMode.Lax;
options.NonceCookie.SecurePolicy = cookiePolicy;
options.CorrelationCookie.SameSite = SameSiteMode.Lax;
options.CorrelationCookie.SecurePolicy = cookiePolicy;
Drei Events vervollständigen den Handler. Das erste fügt die Identifier Ihrer API als resource hinzu (RFC 8707); ohne sie ist das Access Token nicht für Ihre API bestimmt, und die API lehnt es ab. Das zweite prüft den Parameter iss der Autorisierungsantwort (RFC 9207): ASP.NET Core prüft ihn nicht, KeenAuth bietet ihn aber an.
OnRedirectToIdentityProvider = context =>
{
// Build the redirect URI from the setting, not from the Host header of the request.
context.ProtocolMessage.RedirectUri = settings.RedirectUri;
// RFC 8707: an access token for your API.
context.ProtocolMessage.SetParameter("resource", settings.ApiAudience);
return Task.CompletedTask;
},
OnMessageReceived = async context =>
{
// RFC 9207: the authorization response must name the issuer that answered.
var configuration = await context.Options.ConfigurationManager!
.GetConfigurationAsync(context.HttpContext.RequestAborted);
var iss = context.ProtocolMessage.Iss;
if (iss is not null ? iss != configuration.Issuer : configuration.AuthorizationResponseIssParameterSupported)
{
context.Fail("The authorization response has a wrong or missing iss parameter.");
}
},
Das dritte lehnt ein ID Token ab, das nicht von einem Schlüssel Ihrer Umgebung signiert ist. ASP.NET Core überspringt die Signaturprüfung eines ID Tokens, das vom Token-Endpunkt kommt, was OpenID Connect Core über TLS erlaubt; diese App prüft es trotzdem.
OnTokenResponseReceived = async context =>
{
// ASP.NET Core skips the signature check of an ID token that comes from the token endpoint (OpenID
// Connect Core 3.1.3.7 allows it over TLS). This app does not: the token must be signed by a key
// of the environment, with an allowed algorithm.
var configuration = await context.Options.ConfigurationManager!
.GetConfigurationAsync(context.HttpContext.RequestAborted);
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer = configuration.Issuer;
parameters.IssuerSigningKeys = configuration.SigningKeys;
parameters.ValidAudience = settings.ClientId;
var result = await new JsonWebTokenHandler().ValidateTokenAsync(
context.TokenEndpointResponse.IdToken ?? "", parameters);
if (!result.IsValid)
{
context.Fail("The ID token is not signed by a key of the environment.");
}
},
Die letzten beiden Events setzen die Post-Logout-Redirect-URI in die Abmeldeanfrage und schicken eine fehlgeschlagene Anmeldung zur Startseite zurück, ohne den Grund anzuzeigen:
OnRedirectToIdentityProviderForSignOut = context =>
{
context.ProtocolMessage.PostLogoutRedirectUri = settings.PostLogoutRedirectUri;
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
// Do not show the reason in the browser; the logs have it.
context.Response.Redirect("/?error=signin");
context.HandleResponse();
return Task.CompletedTask;
},
5. Anmelden, Seite schützen und abmelden
/login startet die Anmeldung und kehrt danach zum Dashboard zurück, einem festen lokalen Pfad:
public class LoginModel : PageModel
{
/// <summary>Starts sign-in: redirects to KeenAuth, and back to the dashboard afterwards (a fixed local path).</summary>
public IActionResult OnGet() =>
Challenge(new AuthenticationProperties { RedirectUri = "/dashboard" }, OpenIdConnectDefaults.AuthenticationScheme);
}
Das Dashboard erfordert eine Sitzung: Ohne eine startet ASP.NET Core die Anmeldung. Abmelden ist ein POST, und Razor Pages prüft das Antiforgery-Token. Es beendet die Sitzung hier und leitet dann zu KeenAuth weiter (OpenID Connect RP-Initiated Logout mit id_token_hint und post_logout_redirect_uri):
public class LogoutModel : PageModel
{
/// <summary>
/// POST only, and Razor Pages checks the antiforgery token. Ends the session here, then redirects to KeenAuth
/// (OpenID Connect RP-Initiated Logout with id_token_hint and post_logout_redirect_uri).
/// </summary>
public IActionResult OnPost() =>
SignOut(new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
Die Startseite verlinkt zur Anmeldung und sendet das Formular zum Abmelden; der Form-Tag-Helper fügt das Antiforgery-Token hinzu:
@if (User.Identity?.IsAuthenticated == true)
{
<p>Signed in as @Model.DisplayName.</p>
<p><a href="/dashboard">Dashboard</a></p>
<!-- The form tag helper adds the antiforgery token. -->
<form method="post" asp-page="/Logout"><button type="submit">Sign out</button></form>
}
else
{
<p><a href="/login">Sign in</a></p>
}
6. Ihre API aufrufen
Das Dashboard nimmt das Access Token aus der Sitzung und ruft damit Ihre API auf. Das Token bleibt auf dem Server; der Browser erhält nur das HTML.
/// <summary>Needs a session. The access token stays on the server; the browser receives only the HTML.</summary>
[Authorize]
public class DashboardModel(MessagesClient messages) : PageModel
{
public string DisplayName => User.Identity?.Name ?? User.FindFirst("sub")?.Value ?? "";
public int Status { get; private set; }
public string? Messages { get; private set; }
public async Task OnGetAsync()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
(Status, Messages) = await messages.FetchAsync(accessToken ?? "", HttpContext.RequestAborted);
}
}
public async Task<(int Status, string? Body)> FetchAsync(string accessToken, CancellationToken cancel)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "api/messages");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
try
{
using var response = await http.SendAsync(request, cancel);
return ((int)response.StatusCode, response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync(cancel) : null);
}
catch (HttpRequestException)
{
return (502, null);
}
}
Das Beispiel hat kein Refresh Token: Läuft das Access Token ab, antwortet Ihre API mit 401, und das Dashboard zeigt diesen Status an. Melden Sie sich erneut an.
7. App starten
Starten Sie die API aus dem Express-Schnellstart (oder eine beliebige API, die die Tokens akzeptiert) und starten Sie die App mit exportierten Variablen:
ASPNETCORE_URLS=http://localhost:8080 dotnet run --project App
Öffnen Sie http://localhost:8080, wählen Sie Sign in, melden Sie sich bei KeenAuth an und öffnen Sie das Dashboard.
8. Ohne Netzwerk testen
Die Tests benötigen kein Netzwerk und kein KeenAuth-Konto. Ein kleiner Kestrel-Server auf einem Loopback-Port übernimmt die Rolle Ihrer Umgebung (Discovery, ein für den Testlauf erstellter Schlüsselsatz, der Token-Endpunkt, Pushed Authorization Requests, der End-Session-Endpunkt) und Ihrer API. Die Tests starten die echte App auf einem anderen Port und steuern sie mit einem HTTP-Client, der Weiterleitungen nicht folgt, wie ein Browser, den Sie auslesen können:
[Fact]
public async Task APageWithoutASessionStartsSignInAndCallsNoApi()
{
using var response = await h.Get("/dashboard");
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith(h.Fake.Issuer + "/oauth2/authorize?", response.Headers.Location!.ToString());
Assert.Empty(h.Fake.To("/api/messages"));
}
dotnet restore Tests --locked-mode
python3 ../scripts/nuget_deps.py # licences and exact pins (--age: 7-day rule, network)
dotnet test Tests --no-restore
Die Tests prüfen, dass die Konfiguration beim Start validiert wird (und dass kein Fehler ein Secret enthält); dass die Anmeldung PKCE S256, state, nonce und resource sendet und dass der Code mit dem passenden Verifier eingetauscht wird; dass eine Pushed Authorization Request die Parameter aus dem Browser heraushält; dass das Access Token im Header Authorization an die API geht und nie in ein Cookie oder eine Seite gelangt; dass ein unbekannter oder wiederverwendeter State, ein falscher oder fehlender iss, sowie ein ID Token, das unsigniert ist, mit einem anderen Schlüssel signiert wurde, mit einer HMAC signiert wurde, deren Schlüssel der öffentliche Schlüssel ist, oder für eine andere Audience, einen anderen Nonce oder Issuer ausgestellt oder bereits abgelaufen ist, ohne Sitzung abgelehnt werden; dass eine Seite ohne Sitzung die Anmeldung startet; und dass das Abmelden das Antiforgery-Token benötigt, zum End-Session-Endpunkt mit id_token_hint geht und die Sitzung auf dem Server beendet.
Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.
Hinter einem Proxy oder Load Balancer
Setzen Sie APP_BASE_URL auf die öffentliche URL der App; die Redirect-URI wird daraus gebildet, nicht aus dem Header Host. Beendet ein Proxy TLS, fügen Sie die Middleware für weitergeleitete Header nur hinzu, wenn der Proxy der einzige Weg zur App ist, damit die Session-Cookies das Flag Secure erhalten.
Aktuelle Grenzen
- Die Anmeldung startet mit einem
GETauf/login. Die Prüfung vonstateund PKCE schützt den Callback. - Der Handler authentifiziert den Client mit
client_secret_post, nicht mitclient_secret_basic. - Die App akzeptiert ID Tokens, die mit RS256, ES256 oder PS256 signiert sind. KeenAuth kann auch mit EdDSA signieren; dieses Beispiel akzeptiert das nicht, und EdDSA wurde nicht getestet. Behalten Sie für die Umgebung der Webanwendung einen der drei Algorithmen bei.
- Der Ticket-Speicher und die Schlüssel, die das Antiforgery-Token und die State-Cookies schützen, liegen im Speicher eines Prozesses. Nach einem Neustart enden Sitzungen, und laufende Anmeldungen schlagen fehl. Mit mehreren Servern halten Sie die Tickets in einem gemeinsamen Cache und speichern die ASP.NET-Core-Data-Protection-Schlüssel dauerhaft.
- Kein Refresh Token und keine stille Anmeldung.
Nächste Schritte
- Eine Express-API schützen: die API, die diese App aufruft.
- Tokens prüfen: der Verifier unter der Middleware Ihrer API.