ASP.NET Core uygulamasına giriş ekleyin
Bu hızlı başlangıç, ASP.NET Core 10 Razor Pages uygulamasına Microsoft.AspNetCore.Authentication.OpenIdConnect ile KeenAuth girişi ekler. Bu paket, ASP.NET Core'un standart OpenID Connect istemcisidir. KeenAuth'un henüz bir .NET SDK'sı yoktur ve buna ihtiyaç da yoktur: KeenAuth standart bir OpenID Connect sağlayıcısıdır. Uygulamanız OAuth istemcisidir ve tüm token'ları sunucuda tutar; tarayıcıya yalnızca rastgele bir oturum anahtarı taşıyan bir çerez gider. Girişi yapılandırır, bir sayfayı korur, kullanıcının erişim token'ı ile korumalı bir API'yi çağırır, çıkış yapar ve uygulamayı çevrimdışı test edersiniz.
Uygulamanın rotaları şunlardır:
| Rota | Ne yapar |
|---|---|
GET / |
Herkese açık ana sayfa: bir giriş bağlantısı ya da kullanıcının adı ve bir çıkış düğmesi |
GET /login |
Girişi başlatır: KeenAuth'a yönlendirir |
GET /signin-oidc |
OpenID Connect işleyicisi tarafından sunulur: geri çağırma |
GET /dashboard |
Bir oturum gerektirir. Kullanıcıyı ve API'nizin döndürdüğü mesajları gösterir |
POST /logout |
Oturumu burada ve KeenAuth'ta sonlandırır. Antiforgery token'ı gerektirir |
Başlamadan önce
- Bir KeenAuth ortamı. Bu ortamın issuer'ı
https://your-environment.eu.keenauth.appgibi görünür. - Ortamda bir web uygulaması (Uygulama oluşturun) ve bir istemci sırrı. İşleyici sırrı token isteğinin gövdesinde gönderir, bu nedenle
client_secret_postseçin.developmentortamındahttp://localhost/signin-oidcyönlendirme URI'sini vehttp://localhost/çıkış sonrası yönlendirme URI'sini kaydedin; loopback URI'si bağlantı noktası olmadan kaydedilir ve8080gibi herhangi bir bağlantı noktasıyla eşleşir. - Ortamda
messages:readkapsamıyla kayıtlı bir API (API oluşturun). Örneğinhttps://api.example.comolanidentifierdeğeri, erişim token'larının hedef kitlesidir (audience). Express hızlı başlangıcı, bu token'ları kabul eden bir API oluşturur. - .NET 10 SDK (10.0.403 veya daha yeni bir yama sürümü).
1. Paketi ekleyin
Uygulamanın tek bir paketi vardır ve tam bir sürüme sabitlenmiştir. Kilit dosyası (packages.lock.json, RestorePackagesWithLockFile ile etkinleştirilir) diğer tüm paketleri içerik karmasıyla birlikte sabitler.
<!-- 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]" />
Bağımlılık grafiğindeki bir değişikliğin sessizce geçmek yerine başarısız olması için geri yüklemeyi kilitli modda yapın:
dotnet restore --locked-mode
2. Yapılandırın
Uygulama yapılandırmasını yalnızca ortam değişkenlerinden okur. Değerlerini asla commit etmeyin.
| Değişken | Zorunlu | Anlamı |
|---|---|---|
KEENAUTH_ISSUER |
evet | Ortamınızın issuer'ı, https |
KEENAUTH_CLIENT_ID |
evet | Web uygulamasının client_id değeri |
KEENAUTH_CLIENT_SECRET |
evet | Web uygulamasının istemci sırrı |
APP_BASE_URL |
evet | Uygulamanın herkese açık URL'si. Yönlendirme URI'si bundan oluşturulur. http yalnızca localhost, 127.0.0.1 veya [::1] için kullanılabilir |
API_URL |
evet | Uygulamanın çağırdığı API, örneğin http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
evet | KeenAuth'taki bu API'nin tanımlayıcısı |
SESSION_SECURE_COOKIE |
hayır | Yalnızca düz http://localhost için false |
KeenAuthSettings.From, bu değerleri başlangıçta okur ve denetler. Eksik ya da kullanılamaz bir değer uygulamayı durdurur ve hiçbir hata mesajı bir değer içermez.
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. Oturumu sunucuda tutun
Çerez şeması kullanıcıyı uygulamanıza giriş yaptırır. Çerezi yalnızca rastgele bir anahtar taşır; kullanıcı ve token'lar sunucudaki bir bilet deposunda (MemoryTicketStore, tek bir sürecin belleğinde) kalır.
.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 işleyicisini yapılandırın
İşleyici, ortamınızın keşif (<issuer>/.well-known/openid-configuration) bilgilerini ve imza anahtarlarını okur. PKCE (S256), state ve nonce ile yetkilendirme kodu akışını kullanır ve dönüşte bunları denetler. Keşif, itilmiş bir yetkilendirme isteği uç noktası belirtirse işleyici bunu kullanır (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;
İşleyiciyi üç olay tamamlar. Birincisi API'nizin tanımlayıcısını resource (RFC 8707) olarak ekler; bu olmadan erişim token'ı API'niz için amaçlanmamış olur ve API bunu reddeder. İkincisi yetkilendirme yanıtının iss parametresini denetler (RFC 9207): ASP.NET Core bunu denetlemez, KeenAuth ise bunu duyurur.
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.");
}
},
Üçüncü olay, ortamınızın bir anahtarıyla imzalanmamış bir ID token'ı reddeder. ASP.NET Core, token uç noktasından gelen bir ID token'ın imza denetimini atlar; OpenID Connect Core bunu TLS üzerinden izin verir. Bu uygulama yine de denetler.
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.");
}
},
Son iki olay, çıkış isteğine çıkış sonrası yönlendirme URI'sini ekler ve başarısız bir girişi nedenini göstermeden ana sayfaya geri gönderir:
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. Giriş yapın, bir sayfayı koruyun ve çıkış yapın
/login girişi başlatır ve sonrasında sabit bir yerel yol olan panele döner:
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);
}
Panel bir oturum gerektirir: oturum yoksa ASP.NET Core girişi başlatır. Çıkış bir POST isteğidir ve Razor Pages antiforgery token'ını denetler. Oturumu burada sonlandırır, ardından KeenAuth'a yönlendirir (id_token_hint ve post_logout_redirect_uri ile OpenID Connect RP-Initiated Logout):
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);
}
Ana sayfa girişe bağlanır ve çıkış için POST gönderir; form tag helper antiforgery token'ını ekler:
@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. API'nizi çağırın
Panel, erişim token'ını oturumdan alır ve API'nizi bu token ile çağırır. Token sunucuda kalır; tarayıcı yalnızca HTML alır.
/// <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);
}
}
Örnekte yenileme token'ı yoktur: erişim token'ının süresi dolduğunda API'niz 401 yanıtı verir ve panel bu durumu gösterir. Tekrar giriş yapın.
7. Uygulamayı çalıştırın
Hızlı başlangıçtaki Express API'sini (ya da bu token'ları kabul eden herhangi bir API'yi) başlatın ve uygulamayı değişkenler dışa aktarılmış halde çalıştırın:
ASPNETCORE_URLS=http://localhost:8080 dotnet run --project App
http://localhost:8080 adresini açın, Giriş yap seçeneğini seçin, KeenAuth'ta giriş yapın ve paneli açın.
8. Ağ olmadan test edin
Testler ağ bağlantısı ve KeenAuth hesabı gerektirmez. Loopback bağlantı noktasındaki küçük bir Kestrel sunucusu ortamınızı (keşif, test çalıştırması için oluşturulmuş bir anahtar kümesi, token uç noktası, itilmiş yetkilendirme istekleri, oturumu sonlandırma uç noktası) ve API'nizi taklit eder. Testler gerçek uygulamayı başka bir bağlantı noktasında başlatır ve yönlendirmeleri takip etmeyen bir HTTP istemcisiyle sürer; bu istemci, okuyabildiğiniz bir tarayıcı gibidir:
[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
Testler şunları denetler: yapılandırmanın başlangıçta doğrulandığını ve hiçbir hatanın bir sır içermediğini; girişin PKCE S256, state, nonce ve resource gönderdiğini ve kodun eşleşen verifier ile değiştirildiğini; itilmiş bir yetkilendirme isteğinin parametreleri tarayıcıdan uzak tuttuğunu; erişim token'ının API'ye Authorization başlığıyla gittiğini ve hiçbir zaman bir çereze ya da sayfaya yazılmadığını; bilinmeyen ya da yeniden kullanılan bir state, hatalı ya da eksik bir iss, ve imzasız, başka bir anahtarla imzalanmış, genel anahtarla HMAC imzalanmış ya da başka bir hedef kitle, nonce veya issuer için verilmiş ya da süresi dolmuş bir ID token'ın oturum açılmadan reddedildiğini; oturumu olmayan bir sayfanın girişi başlattığını; ve çıkışın antiforgery token'ı gerektirdiğini, id_token_hint ile oturumu sonlandırma uç noktasına gittiğini ve oturumu sunucuda sonlandırdığını.
Gerçek bir tarayıcıyla tam giriş bir KeenAuth ortamı gerektirir. Bu, çevrimdışı testlerin bir parçası değildir.
Proxy veya yük dengeleyicinin arkasında
APP_BASE_URL değerini uygulamanın herkese açık URL'sine ayarlayın; yönlendirme URI'si Host başlığından değil, bundan oluşturulur. Bir proxy TLS'yi sonlandırıyorsa, oturum çerezleri Secure bayrağını alsın diye iletilen başlıklar ara katmanını yalnızca uygulamaya ulaşmanın tek yolu proxy ise ekleyin.
Bugünkü sınırlar
- Giriş,
/loginadresine birGETisteğiyle başlar. Geri çağırmayıstatedenetimi ve PKCE korur. - İşleyici istemciyi
client_secret_basicile değil,client_secret_postile doğrular. - Uygulama RS256, ES256 veya PS256 ile imzalanmış ID token'ları kabul eder. KeenAuth EdDSA ile de imzalayabilir; bu örnek bunu kabul etmez ve EdDSA denenmedi. Web uygulamasının ortamı için bu üç algoritmadan birini tutun.
- Bilet deposu ile CSRF token'ını ve state çerezlerini koruyan anahtarlar tek bir sürecin belleğinde durur. Yeniden başlatmadan sonra oturumlar sona erer ve devam eden girişler başarısız olur. Birden fazla sunucuda, biletleri paylaşılan bir önbellekte tutun ve ASP.NET Core Data Protection anahtarlarını kalıcı hale getirin.
- Yenileme token'ı yoktur ve sessiz oturum açma yoktur.
Sonraki adımlar
- Express API'sini koruyun: bu uygulamanın çağırdığı API.
- Token doğrulama: API'nizin ara katmanının altındaki doğrulayıcı.