Сохранение дополнительных утверждений и маркеров от внешних поставщиков в ASP.NET Core

Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.

Необходимые компоненты

Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье "Проверка подлинности Facebook и Google" в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.

Установка идентификатора клиента и секрета клиента

Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются:

Необязательные утверждения, отправленные в идентификатор или маркер доступа от поставщика проверки подлинности, обычно настраиваются на веб-портале поставщика. Например, Microsoft Azure Active Directory (AAD) позволяет назначать необязательные утверждения маркеру идентификатора приложения в колонке конфигурации маркера регистрации приложения. Дополнительные сведения см. в статье "Практическое руководство. Предоставление дополнительных утверждений приложению (документация Azure)". Для других поставщиков обратитесь к своим внешним наборам документации.

Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным 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.

Установка область проверки подлинности

Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.

Provider Область
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

В примере приложения Google profileemailи openid область автоматически добавляются платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере приложение Google https://www.googleapis.com/auth/user.birthday.read область добавляется для получения дня рождения пользователя:

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

Сопоставление ключей данных пользователя и создание утверждений

В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.

Пример приложения создает утверждения языкового стандарта () и рисунка locale (urn:google:localeurn:google:picture) из данных пользователя Google и picture ключей:

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

В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, а IdentityUser (ApplicationUser) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager<TUser> можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.

В примере приложения OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) устанавливает утверждения языкового стандарта (urn:google:locale) и рисунка (urn:google:picture) для входа ApplicationUser, включая утверждение для GivenName:

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

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

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

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

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

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

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

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

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

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

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

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

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

По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:

  • Браузер обнаруживает, что cookie заголовок слишком длинный.
  • Общий размер запроса слишком велик.

Если для обработки запросов пользователей требуется большое количество данных пользователя:

  • Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
  • Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.

Сохранение маркера доступа

SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.

Пример приложения задает значение SaveTokenstrue в GoogleOptions:

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

При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser's AuthenticationProperties.

Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в 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();
}

Примечание.

Сведения о передаче маркеров Razor компонентам серверного приложения см. в дополнительных сценариях безопасности на стороне Blazor сервера ASP.NET CoreBlazor.

Добавление дополнительных пользовательских маркеров

Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составеSaveTokens, пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.NameTicketCreated:

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

Создание и добавление утверждений

Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.

Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.

Дополнительные сведения см. в разделе Microsoft.AspNetCore.Authentication.OAuth.Claims.

Добавление и обновление утверждений пользователей

Утверждения копируются из внешних поставщиков в пользовательную базу данных при первой регистрации, а не при входе. Если в приложении включены дополнительные утверждения после регистрации пользователя для использования приложения, вызовите SignInManager.RefreshSignInAsync для пользователя принудительное создание новой проверки подлинности cookie.

В среде разработки, работающей с тестовыми учетными записями пользователей, удалите и повторно создайте учетную запись пользователя. Для рабочих систем новые утверждения, добавленные в приложение, можно заполнить в учетные записи пользователей. После создания шаблонов страницы ExternalLogin в приложение Areas/Pages/Identity/Account/Manageдобавьте следующий код ExternalLoginModel в ExternalLogin.cshtml.cs файл.

Добавьте словарь добавленных утверждений. Используйте ключи словаря для хранения типов утверждений и использования значений для хранения значения по умолчанию. Добавьте следующую строку в начало класса. В следующем примере предполагается, что одно утверждение добавляется для изображения Google пользователя с универсальным изображением headshot в качестве значения по умолчанию:

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

Замените код OnGetCallbackAsync метода по умолчанию следующим кодом. Код циклит по словарю утверждений. Утверждения добавляются (резервная копия) или обновляются для каждого пользователя. При добавлении или обновлении утверждений вход пользователя обновляется с помощью SignInManager<TUser>существующего свойства проверки подлинности (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();
    }
}

Аналогичный подход применяется при изменении утверждений во время входа пользователя, но шаг обратной заполнения не требуется. Чтобы обновить утверждения пользователя, вызовите следующее для пользователя:

  • UserManager.ReplaceClaimAsync для утверждений, хранящихся в базе данных удостоверений.
  • SignInManager.RefreshSignInAsync для пользователя, чтобы принудительно создать новую проверку подлинности cookie.

Удаление действий и утверждений утверждений

ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.

Пример выходных данных приложения

Запустите пример приложения и выберите ссылку 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

Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки

Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса (https), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.

Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы (https) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.

Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.

Дополнительные сведения см. в разделе Настройка ASP.NET Core для работы с прокси-серверами и подсистемами балансировки нагрузки.

Просмотреть или скачать образец кода (описание загрузки)

Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.

Просмотреть или скачать образец кода (описание загрузки)

Необходимые компоненты

Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье "Проверка подлинности Facebook и Google" в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.

Установка идентификатора клиента и секрета клиента

Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются к вашему сценарию:

Необязательные утверждения, отправленные в идентификатор или маркер доступа от поставщика проверки подлинности, обычно настраиваются на веб-портале поставщика. Например, Microsoft Azure Active Directory (AAD) позволяет назначать необязательные утверждения маркеру идентификатора приложения в колонке конфигурации маркера регистрации приложения. Дополнительные сведения см. в статье "Практическое руководство. Предоставление дополнительных утверждений приложению (документация Azure)". Для других поставщиков обратитесь к своим внешним наборам документации.

Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным 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;
    };
});

Установка область проверки подлинности

Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.

Provider Область
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

В примере приложения Google profileemailи openid область автоматически добавляются платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере приложение Google https://www.googleapis.com/auth/user.birthday.read область добавляется для получения дня рождения пользователя:

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

Сопоставление ключей данных пользователя и создание утверждений

В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.

Пример приложения создает утверждения языкового стандарта () и рисунка locale (urn:google:localeurn:google:picture) из данных пользователя Google и picture ключей:

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

В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, а IdentityUser (ApplicationUser) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager<TUser> можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.

В примере приложения OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) устанавливает утверждения языкового стандарта (urn:google:locale) и рисунка (urn:google:picture) для входа ApplicationUser, включая утверждение для GivenName:

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

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

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

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

        var result = await _userManager.CreateAsync(user);

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

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

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

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

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

                await _signInManager.SignInAsync(user, props);

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

                return LocalRedirect(returnUrl);
            }
        }

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

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

По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:

  • Браузер обнаруживает, что cookie заголовок слишком длинный.
  • Общий размер запроса слишком велик.

Если для обработки запросов пользователей требуется большое количество данных пользователя:

  • Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
  • Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.

Сохранение маркера доступа

SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.

Пример приложения задает значение SaveTokenstrue в GoogleOptions:

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

При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser's AuthenticationProperties.

Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в 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();
}

Примечание.

Сведения о передаче маркеров Razor компонентам серверного приложения см. в дополнительных сценариях безопасности на стороне Blazor сервера ASP.NET CoreBlazor.

Добавление дополнительных пользовательских маркеров

Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составеSaveTokens, пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.NameTicketCreated:

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

Создание и добавление утверждений

Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.

Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.

Дополнительные сведения см. в разделе Microsoft.AspNetCore.Authentication.OAuth.Claims.

Добавление и обновление утверждений пользователей

Утверждения копируются из внешних поставщиков в пользовательную базу данных при первой регистрации, а не при входе. Если в приложении включены дополнительные утверждения после регистрации пользователя для использования приложения, вызовите SignInManager.RefreshSignInAsync для пользователя принудительное создание новой проверки подлинности cookie.

В среде разработки, работающей с тестовых учетных записей пользователей, можно просто удалить и повторно создать учетную запись пользователя. Для рабочих систем новые утверждения, добавленные в приложение, можно заполнить в учетные записи пользователей. После создания шаблонов страницы ExternalLogin в приложение Areas/Pages/Identity/Account/Manageдобавьте следующий код ExternalLoginModel в ExternalLogin.cshtml.cs файл.

Добавьте словарь добавленных утверждений. Используйте ключи словаря для хранения типов утверждений и использования значений для хранения значения по умолчанию. Добавьте следующую строку в начало класса. В следующем примере предполагается, что одно утверждение добавляется для изображения Google пользователя с универсальным изображением headshot в качестве значения по умолчанию:

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

Замените код OnGetCallbackAsync метода по умолчанию следующим кодом. Код циклит по словарю утверждений. Утверждения добавляются (резервная копия) или обновляются для каждого пользователя. При добавлении или обновлении утверждений вход пользователя обновляется с помощью SignInManager<TUser>существующего свойства проверки подлинности (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();
    }
}

Аналогичный подход применяется при изменении утверждений во время входа пользователя, но шаг обратной заполнения не требуется. Чтобы обновить утверждения пользователя, вызовите следующее для пользователя:

  • UserManager.ReplaceClaimAsync для утверждений, хранящихся в базе данных удостоверений.
  • SignInManager.RefreshSignInAsync для пользователя, чтобы принудительно создать новую проверку подлинности cookie.

Удаление действий и утверждений утверждений

ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.

Пример выходных данных приложения

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

Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки

Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса (https), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.

Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы (https) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.

Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.

Дополнительные сведения см. в разделе Настройка ASP.NET Core для работы с прокси-серверами и подсистемами балансировки нагрузки.

Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.

Просмотреть или скачать образец кода (описание загрузки)

Необходимые компоненты

Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье "Проверка подлинности Facebook и Google" в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.

Установка идентификатора клиента и секрета клиента

Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются к вашему сценарию:

Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным 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;
    };
});

Установка область проверки подлинности

Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.

Provider Область
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

В примере приложения область Google userinfo.profile автоматически добавляется платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере область Google https://www.googleapis.com/auth/user.birthday.read добавляется для получения дня рождения пользователя:

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

Сопоставление ключей данных пользователя и создание утверждений

В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.

Пример приложения создает утверждения языкового стандарта () и рисунка locale (urn:google:localeurn:google:picture) из данных пользователя Google и picture ключей:

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

В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, а IdentityUser (ApplicationUser) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager<TUser> можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.

В примере приложения OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) устанавливает утверждения языкового стандарта (urn:google:locale) и рисунка (urn:google:picture) для входа ApplicationUser, включая утверждение для GivenName:

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

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

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

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

        var result = await _userManager.CreateAsync(user);

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

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

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

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

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

                await _signInManager.SignInAsync(user, props);

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

                return LocalRedirect(returnUrl);
            }
        }

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

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

По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:

  • Браузер обнаруживает, что cookie заголовок слишком длинный.
  • Общий размер запроса слишком велик.

Если для обработки запросов пользователей требуется большое количество данных пользователя:

  • Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
  • Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.

Сохранение маркера доступа

SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.

Пример приложения задает значение SaveTokenstrue в GoogleOptions:

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

При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser's AuthenticationProperties.

Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в 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();
}

Добавление дополнительных пользовательских маркеров

Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составеSaveTokens, пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.NameTicketCreated:

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

Создание и добавление утверждений

Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.

Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.

Дополнительные сведения см. в разделе Microsoft.AspNetCore.Authentication.OAuth.Claims.

Удаление действий и утверждений утверждений

ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.

Пример выходных данных приложения

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

Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки

Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса (https), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.

Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы (https) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.

Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.

Дополнительные сведения см. в разделе Настройка ASP.NET Core для работы с прокси-серверами и подсистемами балансировки нагрузки.

Дополнительные ресурсы