Zum Hauptinhalt springen

Authentifizierung zu deiner .NET Core (Blazor Server)-Anwendung hinzufügen

tipp
  • Die folgende Demonstration basiert auf .NET Core 8.0. Das SDK ist kompatibel mit .NET 6.0 oder höher.
  • Die .NET Core-Beispielprojekte sind im GitHub-Repository verfügbar.

Voraussetzungen

Installation

Füge das NuGet-Paket zu deinem Projekt hinzu:

dotnet add package Logto.AspNetCore.Authentication

Integration

Logto-Authentifizierung hinzufügen

Öffne Startup.cs (oder Program.cs) und füge den folgenden Code hinzu, um die Logto-Authentifizierungsdienste zu registrieren:

Program.cs
using Logto.AspNetCore.Authentication;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLogtoAuthentication(options =>
{
options.Endpoint = builder.Configuration["Logto:Endpoint"]!;
options.AppId = builder.Configuration["Logto:AppId"]!;
options.AppSecret = builder.Configuration["Logto:AppSecret"];
});

Die Methode AddLogtoAuthentication wird Folgendes tun:

  • Das Standard-Authentifizierungsschema auf LogtoDefaults.CookieScheme setzen.
  • Das Standard-Challenge-Schema auf LogtoDefaults.AuthenticationScheme setzen.
  • Das Standard-Abmeldeschema auf LogtoDefaults.AuthenticationScheme setzen.
  • Cookie- und OpenID Connect-Authentifizierungs-Handler zum Authentifizierungsschema hinzufügen.

Anmelde- und Abmeldeflüsse

Bevor wir fortfahren, gibt es zwei verwirrende Begriffe im .NET Core Authentifizierungs-Middleware, die wir klären müssen:

  1. CallbackPath: Die URI, zu der Logto den Benutzer zurückleitet, nachdem der Benutzer sich angemeldet hat (die "Redirect-URI" in Logto)
  2. RedirectUri: Die URI, zu der weitergeleitet wird, nachdem die notwendigen Aktionen im Logto Authentifizierungs-Middleware durchgeführt wurden.

Der Anmeldeprozess kann wie folgt veranschaulicht werden:


Ähnlich hat .NET Core auch SignedOutCallbackPath und RedirectUri für den Abmeldefluss.

Zur Klarheit werden wir sie wie folgt bezeichnen:

Begriff, den wir verwenden.NET Core Begriff
Logto Redirect-URICallbackPath
Logto Post-Abmeldung Redirect-URISignedOutCallbackPath
Anwendungs-Redirect-URIRedirectUri
Bezüglich der umleitungsbasierten Anmeldung
  1. Dieser Authentifizierungsprozess folgt dem OpenID Connect (OIDC) Protokoll, und Logto erzwingt strenge Sicherheitsmaßnahmen, um die Benutzeranmeldung zu schützen.
  2. Wenn du mehrere Apps hast, kannst du denselben Identitätsanbieter (Logto) verwenden. Sobald sich der Benutzer bei einer App anmeldet, wird Logto den Anmeldeprozess automatisch abschließen, wenn der Benutzer auf eine andere App zugreift.

Um mehr über die Gründe und Vorteile der umleitungsbasierten Anmeldung zu erfahren, siehe Logto-Anmeldeerfahrung erklärt.

Redirect-URIs konfigurieren

hinweis

In den folgenden Code-Snippets gehen wir davon aus, dass deine App unter http://localhost:3000/ läuft.

Zuerst konfigurieren wir die Logto-Umleitungs-URI. Füge die folgende URI zur Liste der "Umleitungs-URIs" auf der Logto-Anwendungsdetailseite hinzu:

http://http://localhost:3000//Callback

Um die Logto-Post-Sign-Out-Umleitungs-URI zu konfigurieren, füge die folgende URI zur Liste der "Post-Sign-Out-Umleitungs-URIs" auf der Logto-Anwendungsdetailseite hinzu:

http://http://localhost:3000//SignedOutCallback

Ändere die Standardpfade

Die Logto-Umleitungs-URI hat einen Standardpfad von /Callback, und die Logto-Post-Sign-Out-Umleitungs-URI hat einen Standardpfad von /SignedOutCallback.

Du kannst sie so belassen, wenn es keine besonderen Anforderungen gibt. Wenn du sie ändern möchtest, kannst du die Eigenschaften CallbackPath und SignedOutCallbackPath für LogtoOptions festlegen:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// Andere Konfigurationen...
options.CallbackPath = "/Foo";
options.SignedOutCallbackPath = "/Bar";
});

Denke daran, den Wert auf der Logto-Anwendungsdetailseite entsprechend zu aktualisieren.

Routen hinzufügen

Da Blazor Server SignalR verwendet, um zwischen dem Server und dem Client zu kommunizieren, bedeutet dies, dass Methoden, die den HTTP-Kontext direkt manipulieren (wie das Auslösen von Herausforderungen oder Weiterleitungen), nicht wie erwartet funktionieren, wenn sie von einer Blazor-Komponente aufgerufen werden.

Um dies richtig zu machen, müssen wir explizit zwei Endpunkte für Anmelde- und Abmelde-Weiterleitungen hinzufügen:

Program.cs
app.MapGet("/SignIn", async context =>
{
if (!(context.User?.Identity?.IsAuthenticated ?? false))
{
await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});

app.MapGet("/SignOut", async context =>
{
if (context.User?.Identity?.IsAuthenticated ?? false)
{
await context.SignOutAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});

Jetzt können wir zu diesen Endpunkten weiterleiten, um Anmelde- und Abmeldevorgänge auszulösen.

Anmelde- und Abmeldebuttons implementieren

Im Razor-Komponent fügen Sie den folgenden Code hinzu:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager

@* ... *@

<p>Ist authentifiziert: @User.Identity?.IsAuthenticated</p>
@if (User.Identity?.IsAuthenticated == true)
{
<button @onclick="SignOut">Abmelden</button>
}
else
{
<button @onclick="SignIn">Anmelden</button>
}

@* ... *@

@code {
private ClaimsPrincipal? User { get; set; }

protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}

private void SignIn()
{
NavigationManager.NavigateTo("/SignIn", forceLoad: true);
}

private void SignOut()
{
NavigationManager.NavigateTo("/SignOut", forceLoad: true);
}
}

Erklärung:

  • Der injizierte AuthenticationStateProvider wird verwendet, um den Authentifizierungsstatus des aktuellen Benutzers abzurufen und die User-Eigenschaft zu füllen.
  • Die Methoden SignIn und SignOut werden verwendet, um den Benutzer zu den Anmelde- bzw. Abmeldeendpunkten weiterzuleiten. Aufgrund der Natur von Blazor Server müssen wir NavigationManager mit force load verwenden, um die Weiterleitung auszulösen.

Die Seite zeigt die Schaltfläche "Anmelden" an, wenn der Benutzer nicht authentifiziert ist, und die Schaltfläche "Abmelden", wenn der Benutzer authentifiziert ist.

Die <AuthorizeView />-Komponente

Alternativ kannst du das AuthorizeView-Komponente verwenden, um Inhalte basierend auf dem Authentifizierungsstatus des Benutzers bedingt anzuzeigen. Diese Komponente ist nützlich, wenn du unterschiedlichen Inhalt für authentifizierte und nicht authentifizierte Benutzer anzeigen möchtest.

Füge in deiner Razor-Komponente den folgenden Code hinzu:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authorization

@* ... *@

<AuthorizeView>
<Authorized>
<p>Name: @User?.Identity?.Name</p>
@* Inhalt für authentifizierte Benutzer *@
</Authorized>
<NotAuthorized>
@* Inhalt für nicht authentifizierte Benutzer *@
</NotAuthorized>
</AuthorizeView>

@* ... *@

Die AuthorizeView-Komponente erfordert einen kaskadierenden Parameter vom Typ Task<AuthenticationState>. Eine direkte Möglichkeit, diesen Parameter zu erhalten, besteht darin, die <CascadingAuthenticationState>-Komponente hinzuzufügen. Aufgrund der Natur von Blazor Server können wir die Komponente jedoch nicht einfach zum Layout oder zur Root-Komponente hinzufügen (es könnte nicht wie erwartet funktionieren). Stattdessen können wir den folgenden Code zum Builder (Program.cs oder Startup.cs) hinzufügen, um den kaskadierenden Parameter bereitzustellen:

Program.cs
builder.Services.AddCascadingAuthenticationState();

Dann kannst du die AuthorizeView-Komponente in jeder Komponente verwenden, die sie benötigt.

Checkpoint: Teste deine Anwendung

Jetzt kannst du deine Anwendung testen:

  1. Starte deine Anwendung, du wirst den Anmeldebutton sehen.
  2. Klicke auf den Anmeldebutton, das SDK wird den Anmeldeprozess initiieren und dich zur Logto-Anmeldeseite weiterleiten.
  3. Nachdem du dich angemeldet hast, wirst du zurück zu deiner Anwendung geleitet und siehst den Abmeldebutton.
  4. Klicke auf den Abmeldebutton, um den lokalen Speicher zu leeren und dich abzumelden.

Benutzerinformationen abrufen

Benutzerinformationen anzeigen

Um zu wissen, ob der Benutzer authentifiziert ist, kannst du die Eigenschaft User.Identity?.IsAuthenticated überprüfen.

Um die Benutzerprofilansprüche zu erhalten, kannst du die Eigenschaft User.Claims verwenden:

Controllers/HomeController.cs
var claims = User.Claims;

// Benutzer-ID abrufen
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;

Siehe LogtoParameters.Claims für die Liste der Anspruchsnamen und deren Bedeutungen.

Zusätzliche Ansprüche anfordern

Möglicherweise fehlen einige Benutzerinformationen im zurückgegebenen Objekt von User.Claims. Dies liegt daran, dass OAuth 2.0 und OpenID Connect (OIDC) so konzipiert sind, dass sie dem Prinzip der minimalen Rechte (PoLP) folgen, und Logto auf diesen Standards basiert.

Standardmäßig werden begrenzte Ansprüche zurückgegeben. Wenn du mehr Informationen benötigst, kannst du zusätzliche Berechtigungen anfordern, um auf mehr Ansprüche zuzugreifen.

info

Ein "Anspruch (Claim)" ist eine Behauptung über ein Subjekt; eine "Berechtigung (Scope)" ist eine Gruppe von Ansprüchen. Im aktuellen Fall ist ein Anspruch ein Informationsstück über den Benutzer.

Hier ist ein nicht-normatives Beispiel für die Beziehung zwischen Berechtigung und Anspruch:

tipp

Der "sub"-Anspruch bedeutet "Subjekt", was der eindeutige Identifikator des Benutzers ist (d. h. Benutzer-ID).

Das Logto SDK wird immer drei Berechtigungen anfordern: openid, profile und offline_access.

Um zusätzliche Berechtigungen anzufordern, kannst du die Eigenschaft Scopes im options-Objekt konfigurieren:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Scopes = new string[] {
LogtoParameters.Scopes.Email,
LogtoParameters.Scopes.Phone
}
});

Dann kannst du auf die zusätzlichen Ansprüche über User.Claims zugreifen:

Controllers/HomeController.cs
var claims = User.Claims;

// Benutzer-E-Mail abrufen
var email = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Email)?.Value;

Ansprüche, die eine Netzwerkabfrage benötigen

Um eine Überlastung des Benutzerobjekts zu vermeiden, erfordern einige Ansprüche Netzwerkabfragen, um abgerufen zu werden. Zum Beispiel ist der Anspruch custom_data nicht im Benutzerobjekt enthalten, selbst wenn er in den Berechtigungen angefordert wird. Um diese Ansprüche abzurufen, kannst du GetClaimsFromUserInfoEndpoint auf true im options-Objekt setzen:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.GetClaimsFromUserInfoEndpoint = true;
});

Berechtigungen und Ansprüche

Logto verwendet OIDC Berechtigungen und Ansprüche Konventionen, um die Berechtigungen und Ansprüche für das Abrufen von Benutzerinformationen aus dem ID-Token und dem OIDC userinfo endpoint zu definieren. Sowohl "Berechtigung (Scope)" als auch "Anspruch (Claim)" sind Begriffe aus den OAuth 2.0 und OpenID Connect (OIDC) Spezifikationen.

Hier ist die Liste der unterstützten Berechtigungen (Scopes) und der entsprechenden Ansprüche (Claims):

openid

AnspruchsnameTypBeschreibungBenötigt userinfo?
substringDer eindeutige Identifikator des BenutzersNein

profile

AnspruchsnameTypBeschreibungBenötigt userinfo?
namestringDer vollständige Name des BenutzersNein
usernamestringDer Benutzername des BenutzersNein
picturestringURL des Profilbildes des Endbenutzers. Diese URL MUSS auf eine Bilddatei (zum Beispiel eine PNG-, JPEG- oder GIF-Bilddatei) verweisen und nicht auf eine Webseite, die ein Bild enthält. Beachte, dass diese URL speziell auf ein Profilfoto des Endbenutzers verweisen SOLLTE, das geeignet ist, den Endbenutzer zu beschreiben, und nicht auf ein beliebiges Foto, das vom Endbenutzer aufgenommen wurde.Nein
created_atnumberZeitpunkt, zu dem der Endbenutzer erstellt wurde. Die Zeit wird als Anzahl der Millisekunden seit der Unix-Epoche (1970-01-01T00:00:00Z) dargestellt.Nein
updated_atnumberZeitpunkt, zu dem die Informationen des Endbenutzers zuletzt aktualisiert wurden. Die Zeit wird als Anzahl der Millisekunden seit der Unix-Epoche (1970-01-01T00:00:00Z) dargestellt.Nein

Andere Standardansprüche wie family_name, given_name, middle_name, nickname, preferred_username, profile, website, gender, birthdate, zoneinfo und locale werden ebenfalls im profile-Scope enthalten sein, ohne dass der userinfo-Endpunkt angefordert werden muss. Ein Unterschied im Vergleich zu den oben genannten Ansprüchen besteht darin, dass diese Ansprüche nur zurückgegeben werden, wenn ihre Werte nicht leer sind, während die oben genannten Ansprüche null zurückgeben, wenn die Werte leer sind.

hinweis

Im Gegensatz zu den Standardansprüchen verwenden die Ansprüche created_at und updated_at Millisekunden anstelle von Sekunden.

email

AnspruchsnameTypBeschreibungBenötigt userinfo?
emailstringDie E-Mail-Adresse des BenutzersNein
email_verifiedbooleanOb die E-Mail-Adresse verifiziert wurdeNein

phone

AnspruchsnameTypBeschreibungBenötigt userinfo?
phone_numberstringDie Telefonnummer des BenutzersNein
phone_number_verifiedbooleanOb die Telefonnummer verifiziert wurdeNein

address

Bitte siehe OpenID Connect Core 1.0 für die Details des Adressanspruchs.

custom_data

AnspruchsnameTypBeschreibungBenötigt userinfo?
custom_dataobjectDie benutzerdefinierten Daten des BenutzersJa

identities

AnspruchsnameTypBeschreibungBenötigt userinfo?
identitiesobjectDie verknüpften Identitäten des BenutzersJa
sso_identitiesarrayDie verknüpften SSO-Identitäten des BenutzersJa

urn:logto:scope:organizations

AnspruchsnameTypBeschreibungBenötigt userinfo?
organizationsstring[]Die Organisations-IDs, denen der Benutzer angehörtNein
organization_dataobject[]Die Organisationsdaten, denen der Benutzer angehörtJa

urn:logto:scope:organization_roles

AnspruchsnameTypBeschreibungBenötigt userinfo?
organization_rolesstring[]Die Organisationsrollen, denen der Benutzer angehört, im Format <organization_id>:<role_name>Nein

In Anbetracht der Leistung und der Datengröße bedeutet "Benötigt userinfo?" "Ja", dass der Anspruch nicht im ID-Token angezeigt wird, sondern in der Antwort des userinfo-Endpunkts zurückgegeben wird.

API-Ressourcen

Wir empfehlen, zuerst 🔐 Rollenbasierte Zugangskontrolle (RBAC) zu lesen, um die grundlegenden Konzepte von Logto RBAC zu verstehen und wie man API-Ressourcen richtig einrichtet.

API-Ressource in deiner App konfigurieren

Sobald du die API-Ressourcen eingerichtet hast, kannst du sie bei der Konfiguration von Logto in deiner App hinzufügen:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://<your-api-resource-indicator>";
});

Jede API-Ressource hat ihre eigenen Berechtigungen (Berechtigungen).

Zum Beispiel hat die Ressource https://shopping.your-app.com/api die Berechtigungen shopping:read und shopping:write, und die Ressource https://store.your-app.com/api hat die Berechtigungen store:read und store:write.

Um diese Berechtigungen anzufordern, kannst du sie bei der Konfiguration von Logto in deiner App hinzufügen:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://shopping.your-app.com/api";
options.Scopes = new string[] {
"openid",
"profile",
"offline_access",
"read",
"write"
};
});

Du wirst bemerken, dass Berechtigungen separat von API-Ressourcen definiert sind. Dies liegt daran, dass Resource Indicators for OAuth 2.0 spezifiziert, dass die endgültigen Berechtigungen für die Anfrage das kartesische Produkt aller Berechtigungen bei allen Zielservices sein werden.

hinweis

Es ist in Ordnung, Berechtigungen anzufordern, die in den API-Ressourcen nicht definiert sind. Zum Beispiel kannst du die Berechtigung email anfordern, auch wenn die API-Ressourcen die Berechtigung email nicht verfügbar haben. Nicht verfügbare Berechtigungen werden sicher ignoriert.

Nach der erfolgreichen Anmeldung wird Logto die entsprechenden Berechtigungen an API-Ressourcen gemäß den Rollen des Benutzers ausstellen.

Tokens abrufen

Token über HttpContext abrufen

Manchmal musst du das Zugangstoken oder ID-Token für API-Aufrufe abrufen. Du kannst die Methode GetTokenAsync verwenden, um die Tokens abzurufen:

var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessToken);
var idToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.IdToken);

Du musst dir keine Sorgen über das Ablaufen der Tokens machen, die Authentifizierungsmiddleware wird die Tokens bei Bedarf automatisch auffrischen.

vorsicht

Obwohl die Authentifizierungsmiddleware die Tokens automatisch auffrischt, werden die Ansprüche im Benutzerobjekt aufgrund der Einschränkungen des zugrunde liegenden OpenID Connect-Authentifizierungs-Handlers nicht aktualisiert. Dies kann behoben werden, sobald wir unseren eigenen Authentifizierungs-Handler schreiben.

Beachte, dass das oben genannte Zugangstoken ein opakes Token für den Userinfo-Endpunkt in OpenID Connect ist, welches kein JWT-Token ist. Wenn du die API-Ressource angegeben hast, musst du LogtoParameters.Tokens.AccessTokenForResource verwenden, um das Zugangstoken für die API-Ressource abzurufen:

var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);

Dieses Token wird ein JWT-Token mit der API-Ressource als Zielgruppe sein.

Token in Razor-Komponenten abrufen

Da wir in Razor-Komponenten nicht direkt auf HttpContext zugreifen können, müssen wir den HttpContextAccessor in die Komponente injizieren und ihn verwenden, um die Tokens abzurufen. Der folgende Code zeigt, wie man das Zugangstoken für die API-Ressource in einer Razor-Komponente abruft:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@using Logto.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHttpContextAccessor HttpContextAccessor

@* ... *@

<p><b>Ressource:</b> @(Resource ?? "(null)")</p>
<p><b>Zugangstoken:</b> @(AccessToken ?? "(null)")</p>

@* ... *@

@code {
private ClaimsPrincipal? User { get; set; }
private string? AccessToken { get; set; }
private string? Resource { get; set; }

protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;

if (User?.Identity?.IsAuthenticated == true)
{
await FetchTokenAsync();
}
}

private async Task FetchTokenAsync()
{
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext == null)
{
return;
}

var logtoOptions = httpContext.GetLogtoOptions();
Resource = logtoOptions?.Resource;
// Ersetze durch andere Token-Typen, falls erforderlich
AccessToken = await httpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);
}
}

Weiterführende Lektüre

Endbenutzerflüsse: Authentifizierungsflüsse, Kontoflüsse und Organisationsflüsse Connectors konfigurieren Schütze deine API