Speichern zusätzlicher Ansprüche und Token von externen Anbietern in ASP.NET Core

Eine ASP.NET Core-App kann zusätzliche Ansprüche und Token von externen Authentifizierungsanbietern wie Facebook, Google, Microsoft und Twitter einrichten. Jeder Anbieter zeigt auf seiner Plattform unterschiedliche Informationen über Benutzer*innen an, aber das Muster zum Empfangen und Transformieren von Benutzerdaten in zusätzliche Ansprüche ist identisch.

Voraussetzungen

Entscheiden Sie, welche externen Authentifizierungsanbieter in der App unterstützt werden sollen. Registrieren Sie die App für die einzelnen Anbieter, und rufen Sie eine Client-ID und einen geheimen Clientschlüssel ab. Weitere Informationen finden Sie unter Facebook- und Google-Authentifizierung in ASP.NET Core. Die Beispiel-App verwendet den Google-Authentifizierungsanbieter.

Festlegen der Client-ID und des geheimen Clientschlüssels

Der OAuth-Authentifizierungsanbieter richtet mithilfe einer Client-ID und eines geheimen Clientschlüssels eine Vertrauensstellung mit einer App ein. Die Werte für Client-ID und geheimen Clientschlüssel werden vom externen Authentifizierungsanbieter für die App erstellt, wenn die App beim Anbieter registriert wird. Jeder externe Anbieter, den die App verwendet, muss eigenständig mit der Client-ID und dem geheimen Clientschlüssel des Anbieters konfiguriert werden. Weitere Informationen finden Sie in den relevanten Themen zu externen Authentifizierungsanbietern:

Optionale Ansprüche, die vom Authentifizierungsanbieter in der ID oder dem Zugriffstoken gesendet werden, werden in der Regel im Onlineportal des Anbieters konfiguriert. Beispielsweise ermöglicht Microsoft Azure Active Directory (AAD) das Zuweisen optionaler Ansprüche zum ID-Token der App auf dem Blatt Tokenkonfiguration der App-Registrierung. Weitere Informationen finden Sie unter Gewusst wie: Bereitstellen optionaler Ansprüche für Ihre App (Azure-Dokumentation). Informationen zu anderen Anbietern finden Sie in deren externen Dokumentationsreihen.

Die Beispiel-App konfiguriert den Google-Authentifizierungsanbieter mit einer Client-ID und einem geheimen Clientschlüssel, die bzw. der von Google bereitgestellt wird:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebGoogOauth.Data;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => 
                                  options.SignIn.RequireConfirmedAccount = true)
                                 .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Remaining code removed for brevity.

Einrichten des Authentifizierungsbereichs

Geben Sie die Liste der Berechtigungen an, die vom Anbieter abgerufen werden sollen, indem Sie den Scope angeben. Authentifizierungsbereiche für gängige externe Anbieter werden in der folgenden Tabelle aufgeführt.

Anbieter Bereich
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

In der Beispiel-App werden die Google-Bereiche profile, email und openid automatisch vom Framework hinzugefügt, wenn AddGoogle für AuthenticationBuilder aufgerufen wird. Wenn die App zusätzliche Bereiche erfordert, fügen Sie diese den Optionen hinzu. Im folgenden Beispiel wird der Google-Bereich https://www.googleapis.com/auth/user.birthday.read hinzugefügt, um den Geburtstag von Benutzer*innen abzurufen:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Zuordnen von Benutzerdatenschlüsseln und Erstellen von Ansprüchen

Geben Sie in den Optionen des Anbieters MapJsonKey oder MapJsonSubKey für jeden Schlüssel oder Unterschlüssel in den JSON-Benutzerdaten des externen Anbieters an, damit diese Angaben bei der Anmeldung von der App-Identität gelesen werden können. Weitere Informationen zu Anspruchstypen finden Sie unter ClaimTypes.

Die Beispiel-App erstellt Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) aus den locale- und picture-Schlüsseln in Google-Benutzerdaten:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

In Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync wird ein IdentityUser (ApplicationUser) mit SignInAsync bei der App angemeldet. Während des Anmeldevorgangs kann UserManager<TUser> einen ApplicationUser-Anspruch auf Benutzerdaten speichern, die über den Principal verfügbar sind.

In der Beispiel-App richtet OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) die Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) für den angemeldeten ApplicationUser ein, einschließlich eines Anspruchs für GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}

Standardmäßig werden die Ansprüche von Benutzer*innen im Authentifizierungscookie gespeichert. Wenn das Authentifizierungscookie zu groß ist, kann dies aus folgenden Gründen dazu führen, dass die App fehlschlägt:

  • Der Browser erkennt, dass der cookie-Header zu lang ist.
  • Die Gesamtgröße der Anforderung ist zu groß.

Wenn für die Verarbeitung von Benutzeranforderungen eine große Menge an Benutzerdaten erforderlich ist, gehen Sie folgendermaßen vor:

  • Beschränken Sie die Anzahl und Größe der Benutzeransprüche für die Anforderungsverarbeitung auf die von der App benötigte Menge.
  • Verwenden Sie für den Wert SessionStore der Cookie-Authentifizierungsmiddleware einen benutzerdefinierten Wert für ITicketStore, um die Identität anforderungsübergreifend zu speichern. Bewahren Sie große Mengen von Identitätsinformationen auf dem Server auf, und senden Sie nur einen kleinen Sitzungsbezeichnerschlüssel an den Client.

Speichern des Zugriffstokens

SaveTokens definiert, ob Zugriffs- und Aktualisierungstoken nach einer erfolgreichen Autorisierung in den AuthenticationProperties gespeichert werden sollen. SaveTokens ist standardmäßig auf false festgelegt, um die Größe des endgültigen Authentifizierungscookies zu verringern.

Die Beispiel-App legt den Wert von SaveTokens in GoogleOptions auf true fest:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Speichern Sie bei der Ausführung von OnPostConfirmationAsync das Zugriffstoken (ExternalLoginInfo.AuthenticationTokens) vom externen Anbieter in den AuthenticationProperties für den ApplicationUser.

Die Beispiel-App speichert das Zugriffstoken in OnPostConfirmationAsync (neue Benutzerregistrierung) und OnGetCallbackAsync (frühere Benutzerregistrierung) in Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}

Hinweis

Informationen zur Übergabe von Token an die RazorKomponenten einer serverseitigen BlazorAnwendung finden Sie unter Serverseitige ASP.NET CoreBlazor zusätzliche Sicherheitsszenarien.

Hinzufügen zusätzlicher benutzerdefinierter Token

Um zu veranschaulichen, wie ein benutzerdefiniertes Token hinzugefügt wird, das im Rahmen von SaveTokens gespeichert wird, fügt die Beispiel-App ein AuthenticationToken mit dem aktuellen DateTime-Wert für einen AuthenticationToken.Name von TicketCreated hinzu:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Erstellen und Hinzufügen von Ansprüchen

Das Framework stellt gängige Aktionen und Erweiterungsmethoden bereit, mit denen Ansprüche erstellt und der Auflistung hinzugefügt werden können. Weitere Informationen finden Sie unter ClaimActionCollectionMapExtensions und ClaimActionCollectionUniqueExtensions.

Benutzer*innen können benutzerdefinierte Aktionen definieren, indem sie sie von ClaimAction ableiten und die abstrakte Run-Methode implementieren.

Weitere Informationen finden Sie unter Microsoft.AspNetCore.Authentication.OAuth.Claims.

Hinzufügen und Aktualisieren von Benutzeransprüchen

Ansprüche werden von externen Anbietern bei der ersten Registrierung und nicht bei der Anmeldung in die Benutzerdatenbank kopiert. Wenn zusätzliche Ansprüche in einer App aktiviert werden, nachdem sich Benutzer*innen für die Verwendung der App registriert haben, rufen Sie SignInManager.RefreshSignInAsync für die betreffenden Benutzer*innen auf, um die Generierung eines neuen Authentifizierungscookies zu erzwingen.

Löschen Sie in der Entwicklungsumgebung, die mit Testbenutzerkonten arbeitet, die betreffenden Benutzerkonten, und erstellen Sie sie neu. Für Produktionssysteme können neue Ansprüche, die der App hinzugefügt wurden, mit Benutzerkonten abgeglichen werden. Nach Sie das Gerüst der ExternalLogin-Seite in der App unter Areas/Pages/Identity/Account/Manage erstellt haben, fügen Sie den folgenden Code dem ExternalLoginModel in der Datei ExternalLogin.cshtml.cs hinzu.

Fügen Sie ein Wörterbuch mit hinzugefügten Ansprüchen hinzu. Verwenden Sie die Wörterbuchschlüssel, um die Anspruchstypen zu speichern, und verwenden Sie die Werte zum Speichern eines Standardwerts. Fügen Sie oben in der Klasse die folgende Zeile hinzu. Im folgenden Beispiel wird davon ausgegangen, dass ein Anspruch für das Google-Bild des Benutzers/der Benutzerin mit einem generischen Porträtbild als Standardwert hinzugefügt wird:

private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };

Ersetzen Sie den Standardcode der OnGetCallbackAsync-Methode durch den folgenden Code. Der Code durchläuft das Anspruchswörterbuch in einer Schleife. Ansprüche werden für die einzelnen Benutzer*innen hinzugefügt (abgeglichen) oder aktualisiert. Wenn Ansprüche hinzugefügt oder aktualisiert werden, wird die Benutzeranmeldung mithilfe von SignInManager<TUser> aktualisiert, wobei die vorhandenen Authentifizierungseigenschaften (AuthenticationProperties) beibehalten werden.

private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };

public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider,
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user,
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key,
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }
    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;
        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }
        return Page();
    }
}

Ein ähnlicher Ansatz wird verwendet, wenn sich Ansprüche ändern, während Benutzer*innen angemeldet sind, aber kein Abgleichschritt erforderlich ist. Um die Ansprüche für Benutzer*innen zu aktualisieren, rufen Sie für die betreffenden Benutzer*innen Folgendes auf:

Entfernen von Anspruchsaktionen und Ansprüchen

ClaimActionCollection.Remove(String) entfernt alle Anspruchsaktionen für den angegebenen ClaimType aus der Auflistung. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) löscht einen Anspruch des angegebenen ClaimType aus der Identität. DeleteClaim wird in erster Linie mit OpenID Connect (OIDC) verwendet, um vom Protokoll generierte Ansprüche zu entfernen.

Ausgabe der Beispiel-App

Führen Sie die Beispiel-App aus, und wählen Sie den Link MyClaims aus:

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Weiterleiten von Anforderungsinformationen mit einem Proxy oder Lastenausgleich

Wenn die App hinter einem Proxyserver oder Lastenausgleich bereitgestellt wird, können einige der ursprünglichen Anforderungsinformationen im Anforderungsheader an die App weitergeleitet werden. Zu diesen Informationen gehören in der Regel das sichere Anforderungsschema (https), den Host und die Client-IP-Adresse. Apps lesen diese Anforderungsheader nicht automatisch, um die ursprünglichen Anforderungsinformationen zu ermitteln und zu verwenden.

Das Schema wird bei der Linkgenerierung verwendet, die den Authentifizierungsflow bei externen Anbietern betrifft. Der Verlust des sicheren Schemas (https) führt dazu, dass die App falsche unsichere Umleitungs-URLs generiert.

Verwenden Sie Middleware für weitergeleitete Header, um der App zur Anforderungsverarbeitung die Informationen der ursprünglichen Anforderung verfügbar zu machen.

Weitere Informationen finden Sie unter Konfigurieren von ASP.NET Core für die Arbeit mit Proxyservern und Lastenausgleichen.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Eine ASP.NET Core-App kann zusätzliche Ansprüche und Token von externen Authentifizierungsanbietern wie Facebook, Google, Microsoft und Twitter einrichten. Jeder Anbieter zeigt auf seiner Plattform unterschiedliche Informationen über Benutzer*innen an, aber das Muster zum Empfangen und Transformieren von Benutzerdaten in zusätzliche Ansprüche ist identisch.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Voraussetzungen

Entscheiden Sie, welche externen Authentifizierungsanbieter in der App unterstützt werden sollen. Registrieren Sie die App für die einzelnen Anbieter, und rufen Sie eine Client-ID und einen geheimen Clientschlüssel ab. Weitere Informationen finden Sie unter Facebook- und Google-Authentifizierung in ASP.NET Core. Die Beispiel-App verwendet den Google-Authentifizierungsanbieter.

Festlegen der Client-ID und des geheimen Clientschlüssels

Der OAuth-Authentifizierungsanbieter richtet mithilfe einer Client-ID und eines geheimen Clientschlüssels eine Vertrauensstellung mit einer App ein. Die Werte für Client-ID und geheimen Clientschlüssel werden vom externen Authentifizierungsanbieter für die App erstellt, wenn die App beim Anbieter registriert wird. Jeder externe Anbieter, den die App verwendet, muss eigenständig mit der Client-ID und dem geheimen Clientschlüssel des Anbieters konfiguriert werden. Weitere Informationen finden Sie in den relevanten Themen zu externen Authentifizierungsanbietern, die für Ihr Szenario gelten:

Optionale Ansprüche, die vom Authentifizierungsanbieter in der ID oder dem Zugriffstoken gesendet werden, werden in der Regel im Onlineportal des Anbieters konfiguriert. Beispielsweise ermöglicht Microsoft Azure Active Directory (AAD) das Zuweisen optionaler Ansprüche zum ID-Token der App auf dem Blatt Tokenkonfiguration der App-Registrierung. Weitere Informationen finden Sie unter Gewusst wie: Bereitstellen optionaler Ansprüche für Ihre App (Azure-Dokumentation). Informationen zu anderen Anbietern finden Sie in deren externen Dokumentationsreihen.

Die Beispiel-App konfiguriert den Google-Authentifizierungsanbieter mit einer Client-ID und einem geheimen Clientschlüssel, die bzw. der von Google bereitgestellt wird:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Einrichten des Authentifizierungsbereichs

Geben Sie die Liste der Berechtigungen an, die vom Anbieter abgerufen werden sollen, indem Sie den Scope angeben. Authentifizierungsbereiche für gängige externe Anbieter werden in der folgenden Tabelle aufgeführt.

Anbieter Bereich
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

In der Beispiel-App werden die Google-Bereiche profile, email und openid automatisch vom Framework hinzugefügt, wenn AddGoogle für AuthenticationBuilder aufgerufen wird. Wenn die App zusätzliche Bereiche erfordert, fügen Sie diese den Optionen hinzu. Im folgenden Beispiel wird der Google-Bereich https://www.googleapis.com/auth/user.birthday.read hinzugefügt, um den Geburtstag von Benutzer*innen abzurufen:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Zuordnen von Benutzerdatenschlüsseln und Erstellen von Ansprüchen

Geben Sie in den Optionen des Anbieters MapJsonKey oder MapJsonSubKey für jeden Schlüssel oder Unterschlüssel in den JSON-Benutzerdaten des externen Anbieters an, damit diese Angaben bei der Anmeldung von der App-Identität gelesen werden können. Weitere Informationen zu Anspruchstypen finden Sie unter ClaimTypes.

Die Beispiel-App erstellt Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) aus den locale- und picture-Schlüsseln in Google-Benutzerdaten:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

In Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync wird ein IdentityUser (ApplicationUser) mit SignInAsync bei der App angemeldet. Während des Anmeldevorgangs kann UserManager<TUser> einen ApplicationUser-Anspruch auf Benutzerdaten speichern, die über den Principal verfügbar sind.

In der Beispiel-App richtet OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) die Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) für den angemeldeten ApplicationUser ein, einschließlich eines Anspruchs für GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Standardmäßig werden die Ansprüche von Benutzer*innen im Authentifizierungscookie gespeichert. Wenn das Authentifizierungscookie zu groß ist, kann dies aus folgenden Gründen dazu führen, dass die App fehlschlägt:

  • Der Browser erkennt, dass der cookie-Header zu lang ist.
  • Die Gesamtgröße der Anforderung ist zu groß.

Wenn für die Verarbeitung von Benutzeranforderungen eine große Menge an Benutzerdaten erforderlich ist, gehen Sie folgendermaßen vor:

  • Beschränken Sie die Anzahl und Größe der Benutzeransprüche für die Anforderungsverarbeitung auf die von der App benötigte Menge.
  • Verwenden Sie für den Wert SessionStore der Cookie-Authentifizierungsmiddleware einen benutzerdefinierten Wert für ITicketStore, um die Identität anforderungsübergreifend zu speichern. Bewahren Sie große Mengen von Identitätsinformationen auf dem Server auf, und senden Sie nur einen kleinen Sitzungsbezeichnerschlüssel an den Client.

Speichern des Zugriffstokens

SaveTokens definiert, ob Zugriffs- und Aktualisierungstoken nach einer erfolgreichen Autorisierung in den AuthenticationProperties gespeichert werden sollen. SaveTokens ist standardmäßig auf false festgelegt, um die Größe des endgültigen Authentifizierungscookies zu verringern.

Die Beispiel-App legt den Wert von SaveTokens in GoogleOptions auf true fest:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Speichern Sie bei der Ausführung von OnPostConfirmationAsync das Zugriffstoken (ExternalLoginInfo.AuthenticationTokens) vom externen Anbieter in den AuthenticationProperties für den ApplicationUser.

Die Beispiel-App speichert das Zugriffstoken in OnPostConfirmationAsync (neue Benutzerregistrierung) und OnGetCallbackAsync (frühere Benutzerregistrierung) in Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Hinweis

Informationen zur Übergabe von Token an die RazorKomponenten einer serverseitigen BlazorAnwendung finden Sie unter Serverseitige ASP.NET CoreBlazor zusätzliche Sicherheitsszenarien.

Hinzufügen zusätzlicher benutzerdefinierter Token

Um zu veranschaulichen, wie ein benutzerdefiniertes Token hinzugefügt wird, das im Rahmen von SaveTokens gespeichert wird, fügt die Beispiel-App ein AuthenticationToken mit dem aktuellen DateTime-Wert für einen AuthenticationToken.Name von TicketCreated hinzu:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Erstellen und Hinzufügen von Ansprüchen

Das Framework stellt gängige Aktionen und Erweiterungsmethoden bereit, mit denen Ansprüche erstellt und der Auflistung hinzugefügt werden können. Weitere Informationen finden Sie unter ClaimActionCollectionMapExtensions und ClaimActionCollectionUniqueExtensions.

Benutzer*innen können benutzerdefinierte Aktionen definieren, indem sie sie von ClaimAction ableiten und die abstrakte Run-Methode implementieren.

Weitere Informationen finden Sie unter Microsoft.AspNetCore.Authentication.OAuth.Claims.

Hinzufügen und Aktualisieren von Benutzeransprüchen

Ansprüche werden von externen Anbietern bei der ersten Registrierung und nicht bei der Anmeldung in die Benutzerdatenbank kopiert. Wenn zusätzliche Ansprüche in einer App aktiviert werden, nachdem sich Benutzer*innen für die Verwendung der App registriert haben, rufen Sie SignInManager.RefreshSignInAsync für die betreffenden Benutzer*innen auf, um die Generierung eines neuen Authentifizierungscookies zu erzwingen.

In der Entwicklungsumgebung, die mit Testbenutzerkonten arbeitet, können Sie die betreffenden Benutzerkonten einfach löschen und neu erstellen. Für Produktionssysteme können neue Ansprüche, die der App hinzugefügt wurden, mit Benutzerkonten abgeglichen werden. Nach Sie das Gerüst der ExternalLogin-Seite in der App unter Areas/Pages/Identity/Account/Manage erstellt haben, fügen Sie den folgenden Code dem ExternalLoginModel in der Datei ExternalLogin.cshtml.cs hinzu.

Fügen Sie ein Wörterbuch mit hinzugefügten Ansprüchen hinzu. Verwenden Sie die Wörterbuchschlüssel, um die Anspruchstypen zu speichern, und verwenden Sie die Werte zum Speichern eines Standardwerts. Fügen Sie oben in der Klasse die folgende Zeile hinzu. Im folgenden Beispiel wird davon ausgegangen, dass ein Anspruch für das Google-Bild des Benutzers/der Benutzerin mit einem generischen Porträtbild als Standardwert hinzugefügt wird:

private readonly IReadOnlyDictionary<string, string> _claimsToSync = 
    new Dictionary<string, string>()
    {
        { "urn:google:picture", "https://localhost:5001/headshot.png" },
    };

Ersetzen Sie den Standardcode der OnGetCallbackAsync-Methode durch den folgenden Code. Der Code durchläuft das Anspruchswörterbuch in einer Schleife. Ansprüche werden für die einzelnen Benutzer*innen hinzugefügt (abgeglichen) oder aktualisiert. Wenn Ansprüche hinzugefügt oder aktualisiert werden, wird die Benutzeranmeldung mithilfe von SignInManager<TUser> aktualisiert, wobei die vorhandenen Authentifizierungseigenschaften (AuthenticationProperties) beibehalten werden.

public async Task<IActionResult> OnGetCallbackAsync(
    string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";

        return RedirectToPage("./Login", new {ReturnUrl = returnUrl });
    }

    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a 
    // login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, 
        info.ProviderKey, isPersistent: false, bypassTwoFactor : true);

    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", 
            info.Principal.Identity.Name, info.LoginProvider);

        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, 
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user, 
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, 
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }

    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an 
        // account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;

        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }

        return Page();
    }
}

Ein ähnlicher Ansatz wird verwendet, wenn sich Ansprüche ändern, während Benutzer*innen angemeldet sind, aber kein Abgleichschritt erforderlich ist. Um die Ansprüche für Benutzer*innen zu aktualisieren, rufen Sie für die betreffenden Benutzer*innen Folgendes auf:

Entfernen von Anspruchsaktionen und Ansprüchen

ClaimActionCollection.Remove(String) entfernt alle Anspruchsaktionen für den angegebenen ClaimType aus der Auflistung. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) löscht einen Anspruch des angegebenen ClaimType aus der Identität. DeleteClaim wird in erster Linie mit OpenID Connect (OIDC) verwendet, um vom Protokoll generierte Ansprüche zu entfernen.

Ausgabe der Beispiel-App

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Weiterleiten von Anforderungsinformationen mit einem Proxy oder Lastenausgleich

Wenn die App hinter einem Proxyserver oder Lastenausgleich bereitgestellt wird, können einige der ursprünglichen Anforderungsinformationen im Anforderungsheader an die App weitergeleitet werden. Zu diesen Informationen gehören in der Regel das sichere Anforderungsschema (https), den Host und die Client-IP-Adresse. Apps lesen diese Anforderungsheader nicht automatisch, um die ursprünglichen Anforderungsinformationen zu ermitteln und zu verwenden.

Das Schema wird bei der Linkgenerierung verwendet, die den Authentifizierungsflow bei externen Anbietern betrifft. Der Verlust des sicheren Schemas (https) führt dazu, dass die App falsche unsichere Umleitungs-URLs generiert.

Verwenden Sie Middleware für weitergeleitete Header, um der App zur Anforderungsverarbeitung die Informationen der ursprünglichen Anforderung verfügbar zu machen.

Weitere Informationen finden Sie unter Konfigurieren von ASP.NET Core für die Arbeit mit Proxyservern und Lastenausgleichen.

Eine ASP.NET Core-App kann zusätzliche Ansprüche und Token von externen Authentifizierungsanbietern wie Facebook, Google, Microsoft und Twitter einrichten. Jeder Anbieter zeigt auf seiner Plattform unterschiedliche Informationen über Benutzer*innen an, aber das Muster zum Empfangen und Transformieren von Benutzerdaten in zusätzliche Ansprüche ist identisch.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Voraussetzungen

Entscheiden Sie, welche externen Authentifizierungsanbieter in der App unterstützt werden sollen. Registrieren Sie die App für die einzelnen Anbieter, und rufen Sie eine Client-ID und einen geheimen Clientschlüssel ab. Weitere Informationen finden Sie unter Facebook- und Google-Authentifizierung in ASP.NET Core. Die Beispiel-App verwendet den Google-Authentifizierungsanbieter.

Festlegen der Client-ID und des geheimen Clientschlüssels

Der OAuth-Authentifizierungsanbieter richtet mithilfe einer Client-ID und eines geheimen Clientschlüssels eine Vertrauensstellung mit einer App ein. Die Werte für Client-ID und geheimen Clientschlüssel werden vom externen Authentifizierungsanbieter für die App erstellt, wenn die App beim Anbieter registriert wird. Jeder externe Anbieter, den die App verwendet, muss eigenständig mit der Client-ID und dem geheimen Clientschlüssel des Anbieters konfiguriert werden. Weitere Informationen finden Sie in den relevanten Themen zu externen Authentifizierungsanbietern, die für Ihr Szenario gelten:

Die Beispiel-App konfiguriert den Google-Authentifizierungsanbieter mit einer Client-ID und einem geheimen Clientschlüssel, die bzw. der von Google bereitgestellt wird:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Einrichten des Authentifizierungsbereichs

Geben Sie die Liste der Berechtigungen an, die vom Anbieter abgerufen werden sollen, indem Sie den Scope angeben. Authentifizierungsbereiche für gängige externe Anbieter werden in der folgenden Tabelle aufgeführt.

Anbieter Bereich
Facebook https://www.facebook.com/dialog/oauth
Google https://www.googleapis.com/auth/userinfo.profile
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

In der Beispiel-App wird der Google-Bereich userinfo.profile automatisch vom Framework hinzugefügt, wenn AddGoogle für AuthenticationBuilder aufgerufen wird. Wenn die App zusätzliche Bereiche erfordert, fügen Sie diese den Optionen hinzu. Im folgenden Beispiel wird der Google-Bereich https://www.googleapis.com/auth/user.birthday.read hinzugefügt, um den Geburtstag von Benutzer*innen abzurufen:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Zuordnen von Benutzerdatenschlüsseln und Erstellen von Ansprüchen

Geben Sie in den Optionen des Anbieters MapJsonKey oder MapJsonSubKey für jeden Schlüssel oder Unterschlüssel in den JSON-Benutzerdaten des externen Anbieters an, damit diese Angaben bei der Anmeldung von der App-Identität gelesen werden können. Weitere Informationen zu Anspruchstypen finden Sie unter ClaimTypes.

Die Beispiel-App erstellt Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) aus den locale- und picture-Schlüsseln in Google-Benutzerdaten:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

In Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync wird ein IdentityUser (ApplicationUser) mit SignInAsync bei der App angemeldet. Während des Anmeldevorgangs kann UserManager<TUser> einen ApplicationUser-Anspruch auf Benutzerdaten speichern, die über den Principal verfügbar sind.

In der Beispiel-App richtet OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) die Gebietsschema- (urn:google:locale) und Bildansprüche (urn:google:picture) für den angemeldeten ApplicationUser ein, einschließlich eines Anspruchs für GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Standardmäßig werden die Ansprüche von Benutzer*innen im Authentifizierungscookie gespeichert. Wenn das Authentifizierungscookie zu groß ist, kann dies aus folgenden Gründen dazu führen, dass die App fehlschlägt:

  • Der Browser erkennt, dass der cookie-Header zu lang ist.
  • Die Gesamtgröße der Anforderung ist zu groß.

Wenn für die Verarbeitung von Benutzeranforderungen eine große Menge an Benutzerdaten erforderlich ist, gehen Sie folgendermaßen vor:

  • Beschränken Sie die Anzahl und Größe der Benutzeransprüche für die Anforderungsverarbeitung auf die von der App benötigte Menge.
  • Verwenden Sie für den Wert SessionStore der Cookie-Authentifizierungsmiddleware einen benutzerdefinierten Wert für ITicketStore, um die Identität anforderungsübergreifend zu speichern. Bewahren Sie große Mengen von Identitätsinformationen auf dem Server auf, und senden Sie nur einen kleinen Sitzungsbezeichnerschlüssel an den Client.

Speichern des Zugriffstokens

SaveTokens definiert, ob Zugriffs- und Aktualisierungstoken nach einer erfolgreichen Autorisierung in den AuthenticationProperties gespeichert werden sollen. SaveTokens ist standardmäßig auf false festgelegt, um die Größe des endgültigen Authentifizierungscookies zu verringern.

Die Beispiel-App legt den Wert von SaveTokens in GoogleOptions auf true fest:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Speichern Sie bei der Ausführung von OnPostConfirmationAsync das Zugriffstoken (ExternalLoginInfo.AuthenticationTokens) vom externen Anbieter in den AuthenticationProperties für den ApplicationUser.

Die Beispiel-App speichert das Zugriffstoken in OnPostConfirmationAsync (neue Benutzerregistrierung) und OnGetCallbackAsync (frühere Benutzerregistrierung) in Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Hinzufügen zusätzlicher benutzerdefinierter Token

Um zu veranschaulichen, wie ein benutzerdefiniertes Token hinzugefügt wird, das im Rahmen von SaveTokens gespeichert wird, fügt die Beispiel-App ein AuthenticationToken mit dem aktuellen DateTime-Wert für einen AuthenticationToken.Name von TicketCreated hinzu:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Erstellen und Hinzufügen von Ansprüchen

Das Framework stellt gängige Aktionen und Erweiterungsmethoden bereit, mit denen Ansprüche erstellt und der Auflistung hinzugefügt werden können. Weitere Informationen finden Sie unter ClaimActionCollectionMapExtensions und ClaimActionCollectionUniqueExtensions.

Benutzer*innen können benutzerdefinierte Aktionen definieren, indem sie sie von ClaimAction ableiten und die abstrakte Run-Methode implementieren.

Weitere Informationen finden Sie unter Microsoft.AspNetCore.Authentication.OAuth.Claims.

Entfernen von Anspruchsaktionen und Ansprüchen

ClaimActionCollection.Remove(String) entfernt alle Anspruchsaktionen für den angegebenen ClaimType aus der Auflistung. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) löscht einen Anspruch des angegebenen ClaimType aus der Identität. DeleteClaim wird in erster Linie mit OpenID Connect (OIDC) verwendet, um vom Protokoll generierte Ansprüche zu entfernen.

Ausgabe der Beispiel-App

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Weiterleiten von Anforderungsinformationen mit einem Proxy oder Lastenausgleich

Wenn die App hinter einem Proxyserver oder Lastenausgleich bereitgestellt wird, können einige der ursprünglichen Anforderungsinformationen im Anforderungsheader an die App weitergeleitet werden. Zu diesen Informationen gehören in der Regel das sichere Anforderungsschema (https), den Host und die Client-IP-Adresse. Apps lesen diese Anforderungsheader nicht automatisch, um die ursprünglichen Anforderungsinformationen zu ermitteln und zu verwenden.

Das Schema wird bei der Linkgenerierung verwendet, die den Authentifizierungsflow bei externen Anbietern betrifft. Der Verlust des sicheren Schemas (https) führt dazu, dass die App falsche unsichere Umleitungs-URLs generiert.

Verwenden Sie Middleware für weitergeleitete Header, um der App zur Anforderungsverarbeitung die Informationen der ursprünglichen Anforderung verfügbar zu machen.

Weitere Informationen finden Sie unter Konfigurieren von ASP.NET Core für die Arbeit mit Proxyservern und Lastenausgleichen.

Zusätzliche Ressourcen

  • dotnet/AspNetCore engineering SocialSample-App: Die verknüpfte Beispiel-App befindet sich im Engineering-Branch des GitHub-Repositorys dotnet/AspNetCoremain. Der Branch main enthält Code in der aktiven Entwicklungsphase für das nächste Release von ASP.NET Core. Um eine Version der Beispiel-App für eine veröffentlichte Version von ASP.NET Core anzuzeigen, wählen Sie in der Dropdownliste Branch einen Releasebranch aus (z. B. release/{X.Y}).