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

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

Sonraki adımlar