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

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

Prerequisites

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

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

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

Необязательные утверждения, отправленные поставщиком проверки подлинности в ID или токен доступа, обычно настраиваются на онлайн-портале поставщика. Например, Microsoft Entra ID позволяет назначать необязательные утверждения маркеру идентификатора приложения в панели Token configuration. Дополнительные сведения, см. в разделе Как: предоставить дополнительные утверждения приложению (документация 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().AddGoogleOpenIdConnect(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.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

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

        if (tokens is not null)
        {
            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 Scope
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Майкрософт https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

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

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

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

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

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

builder.Services.AddAuthentication().AddGoogleOpenIdConnect(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.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

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

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});

В Майкрософт.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.

Пример приложения устанавливает значение SaveTokens на true в OpenIdConnectOptions:

builder.Services.AddAuthentication().AddGoogleOpenIdConnect(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.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

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

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});

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

Пример приложения сохраняет маркер доступа в 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();
}

Note

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

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

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

builder.Services.AddAuthentication().AddGoogleOpenIdConnect(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.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

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

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});

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

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

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

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

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

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

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

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

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 Connect (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-адреса.

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

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

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

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

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

Prerequisites

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

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

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

Необязательные утверждения, отправленные поставщиком проверки подлинности в ID или токен доступа, обычно настраиваются на онлайн-портале поставщика. Например, Microsoft Entra ID позволяет назначать необязательные утверждения маркеру идентификатора приложения в колонке Token configuration. Дополнительные сведения, см. в разделе Как: предоставить дополнительные утверждения приложению (документация 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 Scope
Facebook https://www.facebook.com/dialog/oauth
Google profile, email, openid
Майкрософт https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

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

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

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

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

Пример приложения создает претензии на локализацию (urn:google:locale) и изображение (urn:google:picture) из ключей locale и picture в данных пользователя 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;
    };
});

В Майкрософт.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.

Пример приложения устанавливает значение SaveTokens на true в 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) из внешнего поставщика в AuthenticationProperties объекта ApplicationUser.

Пример приложения сохраняет маркер доступа в 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();
}

Note

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

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

Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составе SaveTokens, пример приложения добавляет элемент AuthenticationToken с текущим значением DateTime для AuthenticationToken.Name от 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;
    };
});

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

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

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

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

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

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

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

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

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 Connect (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-адреса.

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

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

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

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

Prerequisites

Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в разделе Использование внешних поставщиков входа с Identity в 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 Scope
Facebook https://www.facebook.com/dialog/oauth
Google https://www.googleapis.com/auth/userinfo.profile
Майкрософт 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");

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

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

Пример приложения создает утверждения языкового стандарта (urn:google:locale) и изображения (urn:google:picture) из ключей locale и picture в данных пользователя 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;
    };
});

В Майкрософт.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) из внешнего поставщика в AuthenticationProperties объекта ApplicationUser.

Пример приложения сохраняет маркер доступа в 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, пример приложения добавляет элемент AuthenticationToken с текущим значением DateTime для AuthenticationToken.Name от 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;
    };
});

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

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

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

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

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

ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType из удостоверения. DeleteClaim в основном используется с OpenID Connect (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-адреса.

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

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

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

  • dotnet/AspNetCore engineering SocialSample app: связанный пример приложения находится в репозитории GitHub dotnet/AspNetCoremain инженерной ветке. Ветвь main содержит код, находящийся под активной разработкой для следующего выпуска ASP.NET Core. Чтобы просмотреть версию примера приложения для выпущенной версии ASP.NET Core, используйте раскрывающийся список Branch, чтобы выбрать ветвь выпуска (например, release/{X.Y}).