
# Aggiungi l'accesso a un'app ASP.NET Core

Questa guida rapida aggiunge l'accesso con KeenAuth a un'app ASP.NET Core 10 Razor Pages usando `Microsoft.AspNetCore.Authentication.OpenIdConnect`, il client OpenID Connect standard di ASP.NET Core. KeenAuth non ha ancora un SDK per .NET, e non serve: KeenAuth è un provider OpenID Connect standard. La tua app è il client OAuth e mantiene tutti i token sul server; il browser riceve solo un cookie che contiene una chiave di sessione casuale. Configuri l'accesso, proteggi una pagina, chiami un'API protetta con l'access token dell'utente, ti disconnetti e testi l'app offline.

L'app ha queste route:

| Route | Cosa fa |
|---|---|
| `GET /` | Home page pubblica: un link per accedere, oppure il nome dell'utente e un pulsante per disconnettersi |
| `GET /login` | Avvia l'accesso: reindirizza a KeenAuth |
| `GET /signin-oidc` | Servita dall'handler OpenID Connect: il callback |
| `GET /dashboard` | Richiede una sessione. Mostra l'utente e i messaggi restituiti dalla tua API |
| `POST /logout` | Termina la sessione qui e su KeenAuth. Richiede il token antiforgery |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha questa forma: `https://your-environment.eu.keenauth.app`.
- Un'applicazione web nell'ambiente ([Crea un'applicazione](/docs/api/management/applications/create)) con un client secret. L'handler invia il secret nel corpo della richiesta al token endpoint, quindi scegli `client_secret_post`. In un ambiente `development`, registra il redirect URI `http://localhost/signin-oidc` e il post-logout redirect URI `http://localhost/`; un URI di loopback viene registrato senza porta e corrisponde a qualsiasi porta, come `8080`.
- Un'API registrata nell'ambiente ([Crea un'API](/docs/api/management/apis/create)) con lo scope `messages:read`. Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei suoi access token. La [guida rapida Express](/docs/quickstarts/express) crea un'API che accetta questi token.
- L'SDK .NET 10 (10.0.403 o una patch successiva).

## 1. Aggiungi il pacchetto

L'app ha un solo pacchetto, fissato a una versione esatta. Il lock file (`packages.lock.json`, attivato con `RestorePackagesWithLockFile`) fissa tutti gli altri pacchetti, con il loro hash del contenuto.

```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]" />
```

Esegui il restore in modalità locked, così una modifica nel grafo delle dipendenze fallisce invece di passare in silenzio:

```sh
dotnet restore --locked-mode
```

## 2. Configura

L'app legge la configurazione solo dalle variabili d'ambiente. Non committare mai i loro valori.

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `KEENAUTH_ISSUER` | sì | L'issuer del tuo ambiente, `https` |
| `KEENAUTH_CLIENT_ID` | sì | Il `client_id` dell'applicazione web |
| `KEENAUTH_CLIENT_SECRET` | sì | Il suo client secret |
| `APP_BASE_URL` | sì | L'URL pubblico dell'app. Il redirect URI viene costruito a partire da esso. `http` solo per `localhost`, `127.0.0.1` o `[::1]` |
| `API_URL` | sì | L'API che l'app chiama, per esempio `http://localhost:3001` |
| `KEENAUTH_API_AUDIENCE` | sì | L'identificatore di quella API in KeenAuth |
| `SESSION_SECURE_COOKIE` | no | `false` solo per `http://localhost` senza TLS |

`KeenAuthSettings.From` legge e verifica questi valori all'avvio. Un valore mancante o non utilizzabile ferma l'app, e nessun messaggio di errore contiene un valore.

```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. Mantieni la sessione sul server

loschema dei cookie accede l'utente alla tua app. Il suo cookie contiene solo una chiave casuale; l'utente e i token restano in un ticket store sul server (`MemoryTicketStore`, in memoria di un singolo processo).

```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. Configura l'handler OpenID Connect

L'handler legge la discovery (`<issuer>/.well-known/openid-configuration`) e le chiavi di firma del tuo ambiente. Usa il flusso authorization code con PKCE (S256), `state` e `nonce`, e li verifica al ritorno. Se la discovery indica un endpoint di pushed authorization request, l'handler lo usa (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;
```

Tre eventi completano l'handler. Il primo aggiunge l'identificatore della tua API come `resource` (RFC 8707); senza di esso, l'access token non è destinato alla tua API e l'API lo rifiuta. Il secondo verifica il parametro `iss` della risposta di autorizzazione (RFC 9207): ASP.NET Core non lo verifica, mentre KeenAuth lo pubblicizza.

```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.");
                        }
                    },
```

Il terzo rifiuta un ID token che non è firmato con una chiave del tuo ambiente. ASP.NET Core salta la verifica della firma di un ID token che proviene dal token endpoint, cosa che OpenID Connect Core consente su TLS; questa app la esegue comunque.

```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.");
                        }
                    },
```

Gli ultimi due eventi inseriscono il post-logout redirect URI nella richiesta di disconnessione e rimandano alla home page un accesso fallito senza mostrare il motivo:

```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. Accedi, proteggi una pagina e disconnettiti

`/login` avvia l'accesso e poi torna alla dashboard, un percorso locale fisso:

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

La dashboard richiede una sessione: senza di essa, ASP.NET Core avvia l'accesso. La disconnessione è una `POST`, e Razor Pages verifica il token antiforgery. Termina la sessione qui, poi reindirizza a KeenAuth (OpenID Connect RP-Initiated Logout con `id_token_hint` e `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);
}
```

La home page contiene il link per accedere e invia un form per disconnettersi; il tag helper del form aggiunge il token antiforgery:

```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. Chiama la tua API

La dashboard prende l'access token dalla sessione e chiama la tua API con esso. Il token resta sul server; il browser riceve solo l'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);
        }
    }
```

L'esempio non ha un refresh token: quando l'access token scade, la tua API risponde `401` e la dashboard mostra quello stato. Accedi di nuovo.

## 7. Esegui l'app

Avvia l'API della guida rapida Express (o qualsiasi API che accetti i token) ed esegui l'app con le variabili esportate:

```sh
ASPNETCORE_URLS=http://localhost:8080 dotnet run --project App
```

Apri `http://localhost:8080`, scegli **Accedi**, accedi su KeenAuth e apri la dashboard.

## 8. Testa senza rete

I test non richiedono rete né un account KeenAuth. Un piccolo server Kestrel su una porta di loopback simula il tuo ambiente (discovery, un set di chiavi creato per l'esecuzione dei test, il token endpoint, le pushed authorization request, l'end-session endpoint) e la tua API. I test avviano l'app reale su un'altra porta e la pilotano con un client HTTP che non segue i redirect, come un browser che puoi leggere:

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

I test verificano che la configurazione sia validata all'avvio (e che nessun errore contenga un secret); che l'accesso invii PKCE S256, `state`, `nonce` e `resource`, e che il codice venga scambiato con il verifier corrispondente; che una pushed authorization request tenga i parametri fuori dal browser; che l'access token arrivi alla API nell'header `Authorization` e non finisca mai in un cookie o in una pagina; che uno state sconosciuto o riutilizzato, un `iss` errato o mancante, e un ID token non firmato, firmato con un'altra chiave, firmato con un HMAC che usa la chiave pubblica, oppure emesso per un'altra audience, un altro nonce o un altro issuer, oppure già scaduto, vengano rifiutati senza creare una sessione; che una pagina senza sessione avvii l'accesso; e che la disconnessione richieda il token antiforgery, vada all'end-session endpoint con `id_token_hint` e termini la sessione sul server.

L'accesso completo con un browser reale richiede un ambiente KeenAuth. Non fa parte dei test offline.

## Dietro un proxy o un load balancer

Imposta `APP_BASE_URL` sull'URL pubblico dell'app; il redirect URI viene costruito a partire da esso, non dall'header `Host`. Se un proxy termina TLS, aggiungi il middleware degli header forwarded solo quando il proxy è l'unico modo per raggiungere l'app, così i cookie di sessione ricevono il flag `Secure`.

## Limiti attuali

- L'accesso inizia con una `GET` a `/login`. La verifica di `state` e PKCE proteggono il callback.
- L'handler autentica il client con `client_secret_post`, non con `client_secret_basic`.
- L'app accetta ID token firmati con RS256, ES256 o PS256. KeenAuth può firmare anche con EdDSA; questo esempio non lo accetta, e EdDSA non è stato provato. Mantieni uno dei tre algoritmi per l'ambiente dell'applicazione web.
- Il ticket store e le chiavi che proteggono il token antiforgery e i cookie di state risiedono nella memoria di un singolo processo. Dopo un riavvio, le sessioni terminano e gli accessi in corso falliscono. Con più server, conserva i ticket in una cache condivisa e rendi persistenti le chiavi di ASP.NET Core Data Protection.
- Nessun refresh token e nessun accesso silenzioso.

## Prossimi passi

- [Proteggi un'API Express](/docs/quickstarts/express): l'API che questa app chiama.
- [Verifica dei token](/docs/sdk/token-verification): il verificatore sotto il middleware della tua API.
