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 profile firmy Google emailopenidsą 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 lub MapJsonSubKey dla każdego klucza lub podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby aplikacja mogła odczytać podczas logowania. 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żyj niestandardowego ITicketStore dla SessionStore oprogramowania pośredniczącego uwierzytelniania Cookie do przechowywania tożsamości między żądaniami. Zachowaj duże ilości informacji o tożsamości 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 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions.

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 roszczenie danego ClaimType z tożsamości. 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 profile firmy Google emailopenidsą 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 lub MapJsonSubKey dla każdego klucza/podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby tożsamość aplikacji mogła odczytać te dane podczas logowania. 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żyj niestandardowego ITicketStore dla SessionStore oprogramowania pośredniczącego uwierzytelniania Cookie do przechowywania tożsamości między żądaniami. Zachowaj duże ilości informacji o tożsamości 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 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions.

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 roszczenie danego ClaimType z tożsamości. 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 lub MapJsonSubKey dla każdego klucza/podklucza w danych użytkownika JSON dostawcy zewnętrznego, aby można było odczytać tożsamość aplikacji przy logowaniu. 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żyj niestandardowego ITicketStore w oprogramowaniu pośredniczącym uwierzytelniania Cookie, aby przechowywać tożsamość za pomocą SessionStore między żądaniami. Zachowaj duże ilości informacji o tożsamości 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 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions.

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 roszczenie owego ClaimType z tożsamości. 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 repozytoriummain 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}).