Udostępnij za pośrednictwem


Utrwalanie dodatkowych oświadczeń i tokenów od dostawców zewnętrznych w usłudze ASP.NET Core

Aplikacja ASP.NET Core może ustanowić dodatkowe oświadczenia i tokeny od zewnętrznych dostawców uwierzytelniania, takich jak Facebook, Google, Microsoft i Twitter. Każdy dostawca ujawnia różne informacje o użytkownikach na swojej platformie, ale wzorzec odbierania i przekształcania danych użytkownika w dodatkowe oświadczenia jest taki sam.

Wymagania wstępne

Zdecyduj, którzy zewnętrzni dostawcy uwierzytelniania mają obsługiwać aplikację. Dla każdego dostawcy zarejestruj aplikację i uzyskaj identyfikator klienta i klucz tajny klienta. Aby uzyskać więcej informacji, zobacz Uwierzytelnianie w usłudze Facebook i Google w usłudze ASP.NET Core. Przykładowa aplikacja używa dostawcy uwierzytelniania Google.

Ustawianie identyfikatora klienta i klucza tajnego klienta

Dostawca uwierzytelniania OAuth ustanawia relację zaufania z aplikacją przy użyciu identyfikatora klienta i klucza tajnego klienta. Wartości identyfikatora klienta i klucza tajnego klienta są tworzone dla aplikacji przez zewnętrznego dostawcę uwierzytelniania po zarejestrowaniu aplikacji u dostawcy. Każdy dostawca zewnętrzny używany przez aplikację musi być skonfigurowany niezależnie z identyfikatorem klienta dostawcy i kluczem tajnym klienta. Aby uzyskać więcej informacji, zobacz tematy dotyczące zewnętrznego dostawcy uwierzytelniania, które mają zastosowanie:

Opcjonalne oświadczenia wysyłane w identyfikatorze lub tokenie dostępu od dostawcy uwierzytelniania są zwykle konfigurowane w portalu online dostawcy. Na przykład identyfikator Entra firmy Microsoft zezwala na przypisywanie opcjonalnych oświadczeń do tokenu identyfikatora aplikacji w bloku Konfiguracja tokenu rejestracji aplikacji. Aby uzyskać więcej informacji, zobacz How to: Provide optional claims to your app (Dokumentacja platformy Azure): Provide optional claims to your app (Dokumentacja platformy Azure). W przypadku innych dostawców zapoznaj się z zewnętrznymi zestawami dokumentacji.

Przykładowa aplikacja konfiguruje dostawcę uwierzytelniania Google przy użyciu identyfikatora klienta i klucza tajnego klienta dostarczonego przez firmę Google:

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.

Ustanawianie zakresu uwierzytelniania

Określ listę uprawnień do pobrania z dostawcy, określając element Scope. Zakresy uwierzytelniania dla typowych dostawców zewnętrznych są wyświetlane w poniższej tabeli.

Dostawca Scope
Facebook https://www.facebook.com/dialog/oauth
Google profile, , emailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

W przykładowej aplikacji zakresy , i openid firmy Google emailprofilesą automatycznie dodawane przez platformę po AddGoogle wywołaniu metody AuthenticationBuilder. Jeśli aplikacja wymaga dodatkowych zakresów, dodaj je do opcji. W poniższym przykładzie zakres Google https://www.googleapis.com/auth/user.birthday.read jest dodawany w celu pobrania urodzin użytkownika:

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

Mapuj klucze danych użytkownika i twórz oświadczenia

W opcjach dostawcy określ MapJsonKey wartość lub MapJsonSubKey dla każdego klucza lub podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby aplikacja identity odczytywała logowanie. Aby uzyskać więcej informacji na temat typów oświadczeń, zobacz ClaimTypes.

Przykładowa aplikacja tworzy ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia z locale kluczy i picture w danych użytkownika Google:

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;
    };
});

W Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsyncpliku (IdentityUserApplicationUser) jest zalogowany do aplikacji przy użyciu SignInAsyncpolecenia . Podczas procesu UserManager<TUser> logowania program może przechowywać ApplicationUser oświadczenia dotyczące danych użytkownika dostępnych w pliku Principal.

W przykładowej aplikacji OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) ustanawia ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia dotyczące zalogowanego elementu ApplicationUser, w tym oświadczenie dla GivenNameelementu :

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();
}

Domyślnie oświadczenia użytkownika są przechowywane w uwierzytelnianiu cookie. Jeśli uwierzytelnianie cookie jest zbyt duże, może to spowodować niepowodzenie aplikacji, ponieważ:

  • Przeglądarka wykrywa, że cookie nagłówek jest za długi.
  • Ogólny rozmiar żądania jest za duży.

Jeśli do przetwarzania żądań użytkowników jest wymagana duża ilość danych użytkownika:

  • Ogranicz liczbę i rozmiar oświadczeń użytkowników na potrzeby przetwarzania żądań tylko do tego, czego wymaga aplikacja.
  • Używanie niestandardowego ITicketStore oprogramowania pośredniczącego Cookie SessionStore uwierzytelniania do przechowywania identity między żądaniami. Zachowaj duże ilości identity informacji na serwerze, wysyłając tylko mały klucz identyfikatora sesji do klienta.

Zapisywanie tokenu dostępu

SaveTokens określa, czy tokeny dostępu i odświeżania powinny być przechowywane w AuthenticationProperties obiekcie po pomyślnym uwierzytelnieniu. SaveTokens parametr jest domyślnie false ustawiony, aby zmniejszyć rozmiar uwierzytelniania cookiekońcowego.

Przykładowa aplikacja ustawia wartość SaveTokens na true w GoogleOptionspliku :

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;
    };
});

Podczas OnPostConfirmationAsync wykonywania zapisz token dostępu (ExternalLoginInfo.AuthenticationTokens) od dostawcy zewnętrznego w pliku ApplicationUserAuthenticationProperties.

Przykładowa aplikacja zapisuje token dostępu w programie OnPostConfirmationAsync (rejestracja nowego użytkownika) i OnGetCallbackAsync (wcześniej zarejestrowanym użytkowniku) w programie 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();
}

Uwaga

Aby uzyskać informacje na temat przekazywania tokenów do Razor składników aplikacji po stronie Blazor serwera, zobacz ASP.NET Podstawowe scenariusze zabezpieczeń i Blazor Web App dodatkowe scenariusze zabezpieczeń.

Jak dodać dodatkowe tokeny niestandardowe

Aby zademonstrować sposób dodawania tokenu niestandardowego, który jest przechowywany jako część SaveTokens, przykładowa aplikacja dodaje element AuthenticationToken z bieżącą DateTime wartością dla AuthenticationToken.Name elementu TicketCreated:

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;
    };
});

Tworzenie i dodawanie oświadczeń

Platforma udostępnia typowe akcje i metody rozszerzenia do tworzenia i dodawania oświadczeń do kolekcji. Aby uzyskać więcej informacji, zobacz i ClaimActionCollectionMapExtensions ClaimActionCollectionUniqueExtensions.

Użytkownicy mogą definiować akcje niestandardowe, wyprowadzając i implementując ClaimAction metodę abstrakcyjną Run .

Aby uzyskać więcej informacji, zobacz Microsoft.AspNetCore.Authentication.OAuth.Claims.

Dodawanie i aktualizowanie oświadczeń użytkowników

Oświadczenia są kopiowane z dostawców zewnętrznych do bazy danych użytkowników podczas pierwszej rejestracji, a nie podczas logowania. Jeśli dodatkowe oświadczenia są włączone w aplikacji po zarejestrowaniu użytkownika w celu korzystania z aplikacji, wywołaj metodę SignInManager.RefreshSignInAsync na użytkowniku, aby wymusić generowanie nowego uwierzytelniania cookie.

W środowisku deweloperów pracującym z kontami użytkowników testowych usuń i ponownie utwórz konto użytkownika. W przypadku systemów produkcyjnych nowe oświadczenia dodane do aplikacji mogą być wypełniane na kontach użytkowników. Po dokonaniu szkieletu ExternalLogin strony w aplikacji pod adresem Areas/Pages/Identity/Account/Managedodaj następujący kod do ExternalLoginModel pliku w pliku ExternalLogin.cshtml.cs .

Dodaj słownik dodanych oświadczeń. Użyj kluczy słownika do przechowywania typów oświadczeń i użyj wartości do przechowywania wartości domyślnej. Dodaj następujący wiersz na początku klasy. W poniższym przykładzie przyjęto założenie, że jedno oświadczenie jest dodawane dla obrazu Google użytkownika z ogólnym obrazem headhot jako wartością domyślną:

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

Zastąp domyślny kod OnGetCallbackAsync metody następującym kodem. Kod jest zapętlany w słowniku oświadczeń. Oświadczenia są dodawane (wypełnione) lub aktualizowane dla każdego użytkownika. Po dodaniu lub zaktualizowaniu oświadczeń logowanie użytkownika jest odświeżane przy użyciu SignInManager<TUser>elementu , zachowując istniejące właściwości uwierzytelniania (AuthenticationProperties).

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();
    }
}

Podobne podejście jest wykonywane, gdy oświadczenia zmieniają się, gdy użytkownik jest zalogowany, ale krok wypełniania nie jest wymagany. Aby zaktualizować oświadczenia użytkownika, wywołaj następujące polecenie dla użytkownika:

Usuwanie akcji i oświadczeń oświadczeń

ClaimActionCollection.Remove(String) usuwa wszystkie akcje oświadczeń dla danej ClaimType kolekcji. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) usuwa oświadczenie podane ClaimType z obiektu identity. DeleteClaim jest używana głównie z protokołem OpenID Connect (OIDC) w celu usunięcia oświadczeń generowanych przez protokół.

Przykładowe dane wyjściowe aplikacji

Uruchom przykładową aplikację i wybierz link MyClaims :

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

Przekazywanie dalej informacji o żądaniu za pomocą serwera proxy lub modułu równoważenia obciążenia

Jeśli aplikacja jest wdrażana za serwerem proxy lub modułem równoważenia obciążenia, niektóre z pierwotnych informacji o żądaniu mogą zostać przekazane dalej do aplikacji w nagłówkach żądania. Te informacje zazwyczaj obejmują bezpieczny schemat żądań (https), hosta i adres IP klienta. Aplikacje nie odczytują automatycznie tych nagłówków żądań, aby odnaleźć pierwotne informacje o żądaniu i z nich korzystać.

Schemat jest używany do generowania linków, które mają wpływ na przepływ uwierzytelniania przy użyciu zewnętrznych dostawców. W wyniku utraty bezpiecznego schematu (https) aplikacja generuje nieprawidłowe niezabezpieczone adresy URL przekierowania.

Użyj oprogramowania pośredniczącego przekazanych nagłówków, aby udostępnić pierwotne informacje o żądaniu do aplikacji w celu przetworzenia żądania.

Aby uzyskać więcej informacji, zobacz Konfigurowanie platformy ASP.NET Core pod kątem pracy z serwerami proxy i modułami równoważenia obciążenia.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Aplikacja ASP.NET Core może ustanowić dodatkowe oświadczenia i tokeny od zewnętrznych dostawców uwierzytelniania, takich jak Facebook, Google, Microsoft i Twitter. Każdy dostawca ujawnia różne informacje o użytkownikach na swojej platformie, ale wzorzec odbierania i przekształcania danych użytkownika w dodatkowe oświadczenia jest taki sam.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Wymagania wstępne

Zdecyduj, którzy zewnętrzni dostawcy uwierzytelniania mają obsługiwać aplikację. Dla każdego dostawcy zarejestruj aplikację i uzyskaj identyfikator klienta i klucz tajny klienta. Aby uzyskać więcej informacji, zobacz Uwierzytelnianie w usłudze Facebook i Google w usłudze ASP.NET Core. Przykładowa aplikacja używa dostawcy uwierzytelniania Google.

Ustawianie identyfikatora klienta i klucza tajnego klienta

Dostawca uwierzytelniania OAuth ustanawia relację zaufania z aplikacją przy użyciu identyfikatora klienta i klucza tajnego klienta. Wartości identyfikatora klienta i klucza tajnego klienta są tworzone dla aplikacji przez zewnętrznego dostawcę uwierzytelniania po zarejestrowaniu aplikacji u dostawcy. Każdy dostawca zewnętrzny używany przez aplikację musi być skonfigurowany niezależnie z identyfikatorem klienta dostawcy i kluczem tajnym klienta. Aby uzyskać więcej informacji, zobacz tematy dotyczące zewnętrznego dostawcy uwierzytelniania, które mają zastosowanie do danego scenariusza:

Opcjonalne oświadczenia wysyłane w identyfikatorze lub tokenie dostępu od dostawcy uwierzytelniania są zwykle konfigurowane w portalu online dostawcy. Na przykład identyfikator Entra firmy Microsoft umożliwia przypisanie opcjonalnych oświadczeń do tokenu identyfikatora aplikacji w bloku Konfiguracja tokenu rejestracji aplikacji. Aby uzyskać więcej informacji, zobacz How to: Provide optional claims to your app (Dokumentacja platformy Azure): Provide optional claims to your app (Dokumentacja platformy Azure). W przypadku innych dostawców zapoznaj się z zewnętrznymi zestawami dokumentacji.

Przykładowa aplikacja konfiguruje dostawcę uwierzytelniania Google przy użyciu identyfikatora klienta i klucza tajnego klienta dostarczonego przez firmę Google:

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;
    };
});

Ustanawianie zakresu uwierzytelniania

Określ listę uprawnień do pobrania z dostawcy, określając element Scope. Zakresy uwierzytelniania dla typowych dostawców zewnętrznych są wyświetlane w poniższej tabeli.

Dostawca Scope
Facebook https://www.facebook.com/dialog/oauth
Google profile, , emailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

W przykładowej aplikacji zakresy , i openid firmy Google emailprofilesą automatycznie dodawane przez platformę po AddGoogle wywołaniu metody AuthenticationBuilder. Jeśli aplikacja wymaga dodatkowych zakresów, dodaj je do opcji. W poniższym przykładzie zakres Google https://www.googleapis.com/auth/user.birthday.read jest dodawany w celu pobrania urodzin użytkownika:

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

Mapuj klucze danych użytkownika i twórz oświadczenia

W opcjach dostawcy określ MapJsonKey wartość lub MapJsonSubKey dla każdego klucza/podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby aplikacja identity odczytywała logowanie. Aby uzyskać więcej informacji na temat typów oświadczeń, zobacz ClaimTypes.

Przykładowa aplikacja tworzy ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia z locale kluczy i picture w danych użytkownika Google:

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;
    };
});

W Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsyncpliku (IdentityUserApplicationUser) jest zalogowany do aplikacji przy użyciu SignInAsyncpolecenia . Podczas procesu UserManager<TUser> logowania program może przechowywać ApplicationUser oświadczenia dotyczące danych użytkownika dostępnych w pliku Principal.

W przykładowej aplikacji OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) ustanawia ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia dotyczące zalogowanego elementu ApplicationUser, w tym oświadczenie dla GivenNameelementu :

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();
}

Domyślnie oświadczenia użytkownika są przechowywane w uwierzytelnianiu cookie. Jeśli uwierzytelnianie cookie jest zbyt duże, może to spowodować niepowodzenie aplikacji, ponieważ:

  • Przeglądarka wykrywa, że cookie nagłówek jest za długi.
  • Ogólny rozmiar żądania jest za duży.

Jeśli do przetwarzania żądań użytkowników jest wymagana duża ilość danych użytkownika:

  • Ogranicz liczbę i rozmiar oświadczeń użytkowników na potrzeby przetwarzania żądań tylko do tego, czego wymaga aplikacja.
  • Używanie niestandardowego ITicketStore oprogramowania pośredniczącego Cookie SessionStore uwierzytelniania do przechowywania identity między żądaniami. Zachowaj duże ilości identity informacji na serwerze, wysyłając tylko mały klucz identyfikatora sesji do klienta.

Zapisywanie tokenu dostępu

SaveTokens określa, czy tokeny dostępu i odświeżania powinny być przechowywane w AuthenticationProperties obiekcie po pomyślnym uwierzytelnieniu. SaveTokens parametr jest domyślnie false ustawiony, aby zmniejszyć rozmiar uwierzytelniania cookiekońcowego.

Przykładowa aplikacja ustawia wartość SaveTokens na true w GoogleOptionspliku :

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;
    };
});

Podczas OnPostConfirmationAsync wykonywania zapisz token dostępu (ExternalLoginInfo.AuthenticationTokens) od dostawcy zewnętrznego w pliku ApplicationUserAuthenticationProperties.

Przykładowa aplikacja zapisuje token dostępu w programie OnPostConfirmationAsync (rejestracja nowego użytkownika) i OnGetCallbackAsync (wcześniej zarejestrowanym użytkowniku) w programie 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();
}

Uwaga

Aby uzyskać informacje na temat przekazywania tokenów do Razor składników aplikacji po stronie Blazor serwera, zobacz ASP.NET Podstawowe scenariusze zabezpieczeń i Blazor Web App dodatkowe scenariusze zabezpieczeń.

Jak dodać dodatkowe tokeny niestandardowe

Aby zademonstrować sposób dodawania tokenu niestandardowego, który jest przechowywany jako część SaveTokens, przykładowa aplikacja dodaje element AuthenticationToken z bieżącą DateTime wartością dla AuthenticationToken.Name elementu TicketCreated:

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;
    };
});

Tworzenie i dodawanie oświadczeń

Platforma udostępnia typowe akcje i metody rozszerzenia do tworzenia i dodawania oświadczeń do kolekcji. Aby uzyskać więcej informacji, zobacz i ClaimActionCollectionMapExtensions ClaimActionCollectionUniqueExtensions.

Użytkownicy mogą definiować akcje niestandardowe, wyprowadzając i implementując ClaimAction metodę abstrakcyjną Run .

Aby uzyskać więcej informacji, zobacz Microsoft.AspNetCore.Authentication.OAuth.Claims.

Dodawanie i aktualizowanie oświadczeń użytkowników

Oświadczenia są kopiowane z dostawców zewnętrznych do bazy danych użytkowników podczas pierwszej rejestracji, a nie podczas logowania. Jeśli dodatkowe oświadczenia są włączone w aplikacji po zarejestrowaniu użytkownika w celu korzystania z aplikacji, wywołaj metodę SignInManager.RefreshSignInAsync na użytkowniku, aby wymusić generowanie nowego uwierzytelniania cookie.

W środowisku deweloperów pracującym z kontami użytkowników testowych możesz po prostu usunąć i ponownie utworzyć konto użytkownika. W przypadku systemów produkcyjnych nowe oświadczenia dodane do aplikacji mogą być wypełniane na kontach użytkowników. Po dokonaniu szkieletu ExternalLogin strony w aplikacji pod adresem Areas/Pages/Identity/Account/Managedodaj następujący kod do ExternalLoginModel pliku w pliku ExternalLogin.cshtml.cs .

Dodaj słownik dodanych oświadczeń. Użyj kluczy słownika do przechowywania typów oświadczeń i użyj wartości do przechowywania wartości domyślnej. Dodaj następujący wiersz na początku klasy. W poniższym przykładzie przyjęto założenie, że jedno oświadczenie jest dodawane dla obrazu Google użytkownika z ogólnym obrazem headhot jako wartością domyślną:

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

Zastąp domyślny kod OnGetCallbackAsync metody następującym kodem. Kod jest zapętlany w słowniku oświadczeń. Oświadczenia są dodawane (wypełnione) lub aktualizowane dla każdego użytkownika. Po dodaniu lub zaktualizowaniu oświadczeń logowanie użytkownika jest odświeżane przy użyciu SignInManager<TUser>elementu , zachowując istniejące właściwości uwierzytelniania (AuthenticationProperties).

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();
    }
}

Podobne podejście jest wykonywane, gdy oświadczenia zmieniają się, gdy użytkownik jest zalogowany, ale krok wypełniania nie jest wymagany. Aby zaktualizować oświadczenia użytkownika, wywołaj następujące polecenie dla użytkownika:

Usuwanie akcji i oświadczeń oświadczeń

ClaimActionCollection.Remove(String) usuwa wszystkie akcje oświadczeń dla danej ClaimType kolekcji. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) usuwa oświadczenie podane ClaimType z obiektu identity. DeleteClaim jest używana głównie z protokołem OpenID Connect (OIDC) w celu usunięcia oświadczeń generowanych przez protokół.

Przykładowe dane wyjściowe aplikacji

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

Przekazywanie dalej informacji o żądaniu za pomocą serwera proxy lub modułu równoważenia obciążenia

Jeśli aplikacja jest wdrażana za serwerem proxy lub modułem równoważenia obciążenia, niektóre z pierwotnych informacji o żądaniu mogą zostać przekazane dalej do aplikacji w nagłówkach żądania. Te informacje zazwyczaj obejmują bezpieczny schemat żądań (https), hosta i adres IP klienta. Aplikacje nie odczytują automatycznie tych nagłówków żądań, aby odnaleźć pierwotne informacje o żądaniu i z nich korzystać.

Schemat jest używany do generowania linków, które mają wpływ na przepływ uwierzytelniania przy użyciu zewnętrznych dostawców. W wyniku utraty bezpiecznego schematu (https) aplikacja generuje nieprawidłowe niezabezpieczone adresy URL przekierowania.

Użyj oprogramowania pośredniczącego przekazanych nagłówków, aby udostępnić pierwotne informacje o żądaniu do aplikacji w celu przetworzenia żądania.

Aby uzyskać więcej informacji, zobacz Konfigurowanie platformy ASP.NET Core pod kątem pracy z serwerami proxy i modułami równoważenia obciążenia.

Aplikacja ASP.NET Core może ustanowić dodatkowe oświadczenia i tokeny od zewnętrznych dostawców uwierzytelniania, takich jak Facebook, Google, Microsoft i Twitter. Każdy dostawca ujawnia różne informacje o użytkownikach na swojej platformie, ale wzorzec odbierania i przekształcania danych użytkownika w dodatkowe oświadczenia jest taki sam.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Wymagania wstępne

Zdecyduj, którzy zewnętrzni dostawcy uwierzytelniania mają obsługiwać aplikację. Dla każdego dostawcy zarejestruj aplikację i uzyskaj identyfikator klienta i klucz tajny klienta. Aby uzyskać więcej informacji, zobacz Uwierzytelnianie w usłudze Facebook i Google w usłudze ASP.NET Core. Przykładowa aplikacja używa dostawcy uwierzytelniania Google.

Ustawianie identyfikatora klienta i klucza tajnego klienta

Dostawca uwierzytelniania OAuth ustanawia relację zaufania z aplikacją przy użyciu identyfikatora klienta i klucza tajnego klienta. Wartości identyfikatora klienta i klucza tajnego klienta są tworzone dla aplikacji przez zewnętrznego dostawcę uwierzytelniania po zarejestrowaniu aplikacji u dostawcy. Każdy dostawca zewnętrzny używany przez aplikację musi być skonfigurowany niezależnie z identyfikatorem klienta dostawcy i kluczem tajnym klienta. Aby uzyskać więcej informacji, zobacz tematy dotyczące zewnętrznego dostawcy uwierzytelniania, które mają zastosowanie do danego scenariusza:

Przykładowa aplikacja konfiguruje dostawcę uwierzytelniania Google przy użyciu identyfikatora klienta i klucza tajnego klienta dostarczonego przez firmę Google:

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;
    };
});

Ustanawianie zakresu uwierzytelniania

Określ listę uprawnień do pobrania z dostawcy, określając element Scope. Zakresy uwierzytelniania dla typowych dostawców zewnętrznych są wyświetlane w poniższej tabeli.

Dostawca Scope
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

W przykładowej aplikacji zakres google userinfo.profile jest automatycznie dodawany przez platformę, gdy AddGoogle jest wywoływany w obiekcie AuthenticationBuilder. Jeśli aplikacja wymaga dodatkowych zakresów, dodaj je do opcji. W poniższym przykładzie zakres Google https://www.googleapis.com/auth/user.birthday.read jest dodawany w celu pobrania urodzin użytkownika:

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

Mapuj klucze danych użytkownika i twórz oświadczenia

W opcjach dostawcy określ MapJsonKey wartość lub MapJsonSubKey dla każdego klucza/podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby aplikacja identity odczytywała logowanie. Aby uzyskać więcej informacji na temat typów oświadczeń, zobacz ClaimTypes.

Przykładowa aplikacja tworzy ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia z locale kluczy i picture w danych użytkownika Google:

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;
    };
});

W Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsyncpliku (IdentityUserApplicationUser) jest zalogowany do aplikacji przy użyciu SignInAsyncpolecenia . Podczas procesu UserManager<TUser> logowania program może przechowywać ApplicationUser oświadczenia dotyczące danych użytkownika dostępnych w pliku Principal.

W przykładowej aplikacji OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) ustanawia ustawienia regionalne (urn:google:locale) i obrazy (urn:google:picture) oświadczenia dotyczące zalogowanego elementu ApplicationUser, w tym oświadczenie dla GivenNameelementu :

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();
}

Domyślnie oświadczenia użytkownika są przechowywane w uwierzytelnianiu cookie. Jeśli uwierzytelnianie cookie jest zbyt duże, może to spowodować niepowodzenie aplikacji, ponieważ:

  • Przeglądarka wykrywa, że cookie nagłówek jest za długi.
  • Ogólny rozmiar żądania jest za duży.

Jeśli do przetwarzania żądań użytkowników jest wymagana duża ilość danych użytkownika:

  • Ogranicz liczbę i rozmiar oświadczeń użytkowników na potrzeby przetwarzania żądań tylko do tego, czego wymaga aplikacja.
  • Używanie niestandardowego ITicketStore oprogramowania pośredniczącego Cookie SessionStore uwierzytelniania do przechowywania identity między żądaniami. Zachowaj duże ilości identity informacji na serwerze, wysyłając tylko mały klucz identyfikatora sesji do klienta.

Zapisywanie tokenu dostępu

SaveTokens określa, czy tokeny dostępu i odświeżania powinny być przechowywane w AuthenticationProperties obiekcie po pomyślnym uwierzytelnieniu. SaveTokens parametr jest domyślnie false ustawiony, aby zmniejszyć rozmiar uwierzytelniania cookiekońcowego.

Przykładowa aplikacja ustawia wartość SaveTokens na true w GoogleOptionspliku :

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;
    };
});

Podczas OnPostConfirmationAsync wykonywania zapisz token dostępu (ExternalLoginInfo.AuthenticationTokens) od dostawcy zewnętrznego w pliku ApplicationUserAuthenticationProperties.

Przykładowa aplikacja zapisuje token dostępu w programie OnPostConfirmationAsync (rejestracja nowego użytkownika) i OnGetCallbackAsync (wcześniej zarejestrowanym użytkowniku) w programie 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();
}

Jak dodać dodatkowe tokeny niestandardowe

Aby zademonstrować sposób dodawania tokenu niestandardowego, który jest przechowywany jako część SaveTokens, przykładowa aplikacja dodaje element AuthenticationToken z bieżącą DateTime wartością dla AuthenticationToken.Name elementu TicketCreated:

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;
    };
});

Tworzenie i dodawanie oświadczeń

Platforma udostępnia typowe akcje i metody rozszerzenia do tworzenia i dodawania oświadczeń do kolekcji. Aby uzyskać więcej informacji, zobacz i ClaimActionCollectionMapExtensions ClaimActionCollectionUniqueExtensions.

Użytkownicy mogą definiować akcje niestandardowe, wyprowadzając i implementując ClaimAction metodę abstrakcyjną Run .

Aby uzyskać więcej informacji, zobacz Microsoft.AspNetCore.Authentication.OAuth.Claims.

Usuwanie akcji i oświadczeń oświadczeń

ClaimActionCollection.Remove(String) usuwa wszystkie akcje oświadczeń dla danej ClaimType kolekcji. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) usuwa oświadczenie podane ClaimType z obiektu identity. DeleteClaim jest używana głównie z protokołem OpenID Connect (OIDC) w celu usunięcia oświadczeń generowanych przez protokół.

Przykładowe dane wyjściowe aplikacji

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

Przekazywanie dalej informacji o żądaniu za pomocą serwera proxy lub modułu równoważenia obciążenia

Jeśli aplikacja jest wdrażana za serwerem proxy lub modułem równoważenia obciążenia, niektóre z pierwotnych informacji o żądaniu mogą zostać przekazane dalej do aplikacji w nagłówkach żądania. Te informacje zazwyczaj obejmują bezpieczny schemat żądań (https), hosta i adres IP klienta. Aplikacje nie odczytują automatycznie tych nagłówków żądań, aby odnaleźć pierwotne informacje o żądaniu i z nich korzystać.

Schemat jest używany do generowania linków, które mają wpływ na przepływ uwierzytelniania przy użyciu zewnętrznych dostawców. W wyniku utraty bezpiecznego schematu (https) aplikacja generuje nieprawidłowe niezabezpieczone adresy URL przekierowania.

Użyj oprogramowania pośredniczącego przekazanych nagłówków, aby udostępnić pierwotne informacje o żądaniu do aplikacji w celu przetworzenia żądania.

Aby uzyskać więcej informacji, zobacz Konfigurowanie platformy ASP.NET Core pod kątem pracy z serwerami proxy i modułami równoważenia obciążenia.

Dodatkowe zasoby

  • dotnet/AspNetCore engineering SocialSample app: połączona przykładowa aplikacja znajduje się w repozytorium main github dotnet/AspNetCore. Gałąź main zawiera kod w ramach aktywnego programowania dla następnej wersji ASP.NET Core. Aby wyświetlić wersję przykładowej aplikacji dla wydanej wersji ASP.NET Core, użyj listy rozwijanej Gałąź , aby wybrać gałąź wydania (na przykład release/{X.Y}).