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:
- Authentifizierung über Facebook
- Authentifizierung über Google
- Microsoft-Authentifizierung
- Authentifizierung über Twitter
- Andere Authentifizierungsanbieter
- OpenIdConnect
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 Entra ID 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 |
---|---|
https://www.facebook.com/dialog/oauth |
|
profile , email openid |
|
Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
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-identity 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 einen benutzerdefinierten ITicketStore für die Cookie Authentifzierungsmiddleware SessionStore, um identity über die Anforderungen hinweg zu speichern. Bewahren Sie große Mengen von Informationen zur identity 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 zum Übergeben von Token an die Razor Komponenten einer serverseitigen Blazor App finden Sie unter ASP.NET Core serverseitigen und Blazor Web App zusätzlichen 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:
- UserManager.ReplaceClaimAsync für die jeweiligen Benutzer*innen für Ansprüche, die in der identitysdatenbank gespeichert sind.
- SignInManager.RefreshSignInAsync für die Benutzer*innen, um die Generierung eines neuen Authentifizierungcookies zu erzwingen.
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 identity. 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:
- Authentifizierung über Facebook
- Authentifizierung über Google
- Microsoft-Authentifizierung
- Authentifizierung über Twitter
- Andere Authentifizierungsanbieter
- OpenIdConnect
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 Entra ID Ihnen 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 |
---|---|
https://www.facebook.com/dialog/oauth |
|
profile , email openid |
|
Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
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 einen benutzerdefinierten ITicketStore für die Cookie Authentifzierungsmiddleware SessionStore, um identity über die Anforderungen hinweg zu speichern. Bewahren Sie große Mengen von Informationen zur identity 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 zum Übergeben von Token an die Razor Komponenten einer serverseitigen Blazor App finden Sie unter ASP.NET Core serverseitigen und Blazor Web App zusätzlichen 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:
- UserManager.ReplaceClaimAsync für die jeweiligen Benutzer*innen für Ansprüche, die in der identitysdatenbank gespeichert sind.
- SignInManager.RefreshSignInAsync für die Benutzer*innen, um die Generierung eines neuen Authentifizierungcookies zu erzwingen.
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 identity. 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:
- Authentifizierung über Facebook
- Authentifizierung über Google
- Microsoft-Authentifizierung
- Authentifizierung über Twitter
- Andere Authentifizierungsanbieter
- OpenIdConnect
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 |
---|---|
https://www.facebook.com/dialog/oauth |
|
https://www.googleapis.com/auth/userinfo.profile |
|
Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
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 einen benutzerdefinierten ITicketStore für die Cookie Authentifzierungsmiddleware SessionStore, um identity über die Anforderungen hinweg zu speichern. Bewahren Sie große Mengen von Informationen zur identity 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 identity. 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/AspNetCore
main
. Der Branchmain
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}
).