Ajouter la connexion à une application ASP.NET Core
Ce guide de démarrage rapide ajoute la connexion KeenAuth à une application ASP.NET Core 10 Razor Pages avec Microsoft.AspNetCore.Authentication.OpenIdConnect, le client OpenID Connect standard d'ASP.NET Core. KeenAuth ne dispose pas encore de SDK .NET, et il n'en faut pas : KeenAuth est un fournisseur OpenID Connect standard. Votre application est le client OAuth et conserve tous les jetons côté serveur ; le navigateur ne reçoit qu'un cookie contenant une clé de session aléatoire. Vous configurez la connexion, protégez une page, appelez une API protégée avec le jeton d'accès de l'utilisateur, vous déconnectez et testez l'application hors ligne.
L'application comporte ces routes :
| Route | Fonction |
|---|---|
GET / |
Page d'accueil publique : un lien de connexion, ou le nom de l'utilisateur et un bouton de déconnexion |
GET /login |
Lance la connexion : redirige vers KeenAuth |
GET /signin-oidc |
Servi par le gestionnaire OpenID Connect : le rappel |
GET /dashboard |
Nécessite une session. Affiche l'utilisateur et les messages que renvoie votre API |
POST /logout |
Met fin à la session ici et chez KeenAuth. Nécessite le jeton antifalsification |
Avant de commencer
- Un environnement KeenAuth. Son émetteur ressemble à
https://your-environment.eu.keenauth.app. - Une application web dans l'environnement (Créer une application) avec un secret client. Le gestionnaire envoie le secret dans le corps de la requête de jeton, donc choisissez
client_secret_post. Dans un environnementdevelopment, enregistrez l'URI de redirectionhttp://localhost/signin-oidcet l'URI de redirection après déconnexionhttp://localhost/; un URI de boucle locale est enregistré sans port et correspond à n'importe quel port, comme8080. - Une API enregistrée dans l'environnement (Créer une API) avec la portée
messages:read. Son identifiantidentifier, par exemplehttps://api.example.com, est l'audience de ses jetons d'accès. Le guide de démarrage rapide Express crée une API qui accepte ces jetons. - Le SDK .NET 10 (10.0.403 ou un correctif ultérieur).
1. Ajouter le paquet
L'application n'a qu'un seul paquet, épinglé à une version exacte. Le fichier de verrouillage (packages.lock.json, activé avec RestorePackagesWithLockFile) épingle tous les autres paquets, avec leur empreinte de contenu.
<!-- 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]" />
Restaurez en mode verrouillé, afin qu'une modification du graphe de dépendances échoue au lieu de passer silencieusement :
dotnet restore --locked-mode
2. Configurer
L'application lit sa configuration uniquement depuis des variables d'environnement. Ne validez jamais leurs valeurs dans le dépôt.
| Variable | Obligatoire | Signification |
|---|---|---|
KEENAUTH_ISSUER |
oui | L'émetteur de votre environnement, https |
KEENAUTH_CLIENT_ID |
oui | Le client_id de l'application web |
KEENAUTH_CLIENT_SECRET |
oui | Son secret client |
APP_BASE_URL |
oui | L'URL publique de l'application. L'URI de redirection est construit à partir d'elle. http uniquement pour localhost, 127.0.0.1 ou [::1] |
API_URL |
oui | L'API que l'application appelle, par exemple http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
oui | L'identifiant de cette API dans KeenAuth |
SESSION_SECURE_COOKIE |
non | false uniquement pour http://localhost en clair |
KeenAuthSettings.From lit et vérifie ces valeurs au démarrage. Une valeur absente ou inutilisable arrête l'application, et aucun message d'erreur ne contient de valeur.
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. Garder la session sur le serveur
Le schéma de cookie connecte l'utilisateur à votre application. Son cookie ne contient qu'une clé aléatoire ; l'utilisateur et les jetons restent dans un magasin de tickets sur le serveur (MemoryTicketStore, en mémoire d'un processus).
.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. Configurer le gestionnaire OpenID Connect
Le gestionnaire lit la découverte (<issuer>/.well-known/openid-configuration) et les clés de signature de votre environnement. Il utilise le flux de code d'autorisation avec PKCE (S256), state et nonce, et les vérifie au retour. Si la découverte nomme un point de terminaison de requête d'autorisation poussée, le gestionnaire l'utilise (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;
Trois événements complètent le gestionnaire. Le premier ajoute l'identifiant de votre API comme resource (RFC 8707) ; sans lui, le jeton d'accès n'est pas destiné à votre API et l'API le rejette. Le second vérifie le paramètre iss de la réponse d'autorisation (RFC 9207) : ASP.NET Core ne le vérifie pas, et KeenAuth l'annonce.
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.");
}
},
Le troisième refuse un jeton ID qui n'est pas signé par une clé de votre environnement. ASP.NET Core ignore la vérification de signature d'un jeton ID provenant du point de terminaison de jeton, ce que OpenID Connect Core autorise sur TLS ; cette application la effectue quand même.
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.");
}
},
Les deux derniers événements placent l'URI de redirection après déconnexion dans la requête de déconnexion et renvoient une connexion échouée vers la page d'accueil sans en afficher la raison :
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. Se connecter, protéger une page et se déconnecter
/login lance la connexion et revient ensuite au tableau de bord, un chemin local fixe :
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);
}
Le tableau de bord nécessite une session : sans session, ASP.NET Core lance la connexion. La déconnexion est une POST, et Razor Pages vérifie le jeton antifalsification. Elle met fin à la session ici, puis redirige vers KeenAuth (OpenID Connect RP-Initiated Logout avec id_token_hint et 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);
}
La page d'accueil renvoie vers la connexion et envoie un formulaire vers la déconnexion ; l'assistant de balise form ajoute le jeton antifalsification :
@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. Appeler votre API
Le tableau de bord prend le jeton d'accès dans la session et appelle votre API avec lui. Le jeton reste sur le serveur ; le navigateur ne reçoit que le 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);
}
}
L'exemple n'a pas de jeton de rafraîchissement : lorsque le jeton d'accès expire, votre API répond 401 et le tableau de bord affiche ce statut. Reconnectez-vous.
7. Lancer l'application
Démarrez l'API du guide Express (ou toute API qui accepte les jetons) et lancez l'application avec les variables exportées :
ASPNETCORE_URLS=http://localhost:8080 dotnet run --project App
Ouvrez http://localhost:8080, choisissez Se connecter, connectez-vous chez KeenAuth et ouvrez le tableau de bord.
8. Tester sans réseau
Les tests n'ont besoin ni de réseau ni de compte KeenAuth. Un petit serveur Kestrel sur un port de boucle locale joue le rôle de votre environnement (découverte, ensemble de clés créé pour l'exécution des tests, point de terminaison de jeton, requêtes d'autorisation poussées, point de terminaison de fin de session) et de votre API. Les tests démarrent la véritable application sur un autre port et la pilotent avec un client HTTP qui ne suit pas les redirections, comme un navigateur que vous pouvez lire :
[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
Les tests vérifient que la configuration est validée au démarrage (et qu'aucune erreur ne contient de secret) ; que la connexion envoie PKCE S256, state, nonce et resource, et que le code est échangé avec le vérificateur correspondant ; qu'une requête d'autorisation poussée garde les paramètres hors du navigateur ; que le jeton d'accès est envoyé à l'API dans l'en-tête Authorization et n'est jamais placé dans un cookie ou une page ; qu'un état inconnu ou réutilisé, un iss erroné ou absent, et un jeton ID non signé, signé avec une autre clé, signé avec un HMAC utilisant la clé publique, ou émis pour une autre audience, un autre nonce ou un autre émetteur, ou déjà expiré, sont refusés sans session ; qu'une page sans session lance la connexion ; et que la déconnexion nécessite le jeton antifalsification, passe par le point de terminaison de fin de session avec id_token_hint et met fin à la session sur le serveur.
La connexion complète avec un vrai navigateur nécessite un environnement KeenAuth. Elle ne fait pas partie des tests hors ligne.
Derrière un proxy ou un équilibreur de charge
Définissez APP_BASE_URL sur l'URL publique de l'application ; l'URI de redirection est construit à partir d'elle, et non à partir de l'en-tête Host. Si un proxy termine le TLS, ajoutez le middleware des en-têtes transmis seulement lorsque le proxy est le seul chemin d'accès à l'application, afin que les cookies de session reçoivent l'indicateur Secure.
Limites actuelles
- La connexion commence par une
GETvers/login. La vérificationstateet PKCE protègent le rappel. - Le gestionnaire authentifie le client avec
client_secret_post, et nonclient_secret_basic. - L'application accepte les jetons ID signés avec RS256, ES256 ou PS256. KeenAuth peut aussi signer avec EdDSA ; cet exemple ne l'accepte pas, et EdDSA n'a pas été testé. Conservez l'un des trois algorithmes pour l'environnement de l'application web.
- Le magasin de tickets et les clés qui protègent le jeton antifalsification et les cookies d'état résident en mémoire d'un processus. Après un redémarrage, les sessions prennent fin et les connexions en cours échouent. Avec plusieurs serveurs, conservez les tickets dans un cache partagé et persistez les clés ASP.NET Core Data Protection.
- Pas de jeton de rafraîchissement ni de connexion silencieuse.
Étapes suivantes
- Protéger une API Express : l'API que cette application appelle.
- Vérifier les jetons : le vérificateur sous le middleware de votre API.