
# 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](/docs/api/management/applications/create)) mit einem Client Secret. Der Handler sendet das Secret im Body der Token-Anfrage, wählen Sie daher `client_secret_post`. Registrieren Sie in einer `development`-Umgebung die Redirect-URI `http://localhost/signin-oidc` und die Post-Logout-Redirect-URI `http://localhost/`; eine Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa `8080`.
- Eine API, die in der Umgebung registriert ist ([API anlegen](/docs/api/management/apis/create)) mit dem Scope `messages:read`. Ihr `identifier`, zum Beispiel `https://api.example.com`, ist die Audience ihrer Access Tokens. Der [Express-Schnellstart](/docs/quickstarts/express) 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.

```xml
    <!-- 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:

```sh
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.

```csharp
    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).

```csharp
            .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).

```csharp
                // 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.

```csharp
                    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.

```csharp
                    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:

```csharp
                    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:

```csharp
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`):

```csharp
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:

```cshtml
    @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.

```csharp
/// <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);
    }
}
```

```csharp
    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:

```sh
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:

```csharp
    [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"));
    }
```

```sh
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 `GET` auf `/login`. Die Prüfung von `state` und PKCE schützt den Callback.
- Der Handler authentifiziert den Client mit `client_secret_post`, nicht mit `client_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](/docs/quickstarts/express): die API, die diese App aufruft.
- [Tokens prüfen](/docs/sdk/token-verification): der Verifier unter der Middleware Ihrer API.
