Compartilhar via


Persistir declarações e tokens adicionais de provedores externos no ASP.NET Core

Um aplicativo ASP.NET Core pode estabelecer declarações e tokens adicionais de provedores de autenticação externa, como Facebook, Google, Microsoft e Twitter. Cada provedor revela informações diferentes sobre os usuários em sua plataforma, mas o padrão para receber e transformar dados do usuário em declarações adicionais é o mesmo.

Pré-requisitos

Decida quais provedores de autenticação externa serão compatíveis com o aplicativo. Para cada provedor, registre o aplicativo e obtenha uma ID do cliente e um segredo do cliente. Para obter mais informações, confira Autenticação do Facebook e do Google no ASP.NET Core. O aplicativo de exemplo usa o provedor de autenticação do Google.

Defina a ID do cliente e o segredo do cliente

O provedor de autenticação OAuth estabelece uma relação de confiança com um aplicativo usando uma ID do cliente e um segredo do cliente. Os valores de ID do cliente e segredo do cliente são criados para o aplicativo pelo provedor de autenticação externa quando o aplicativo é registrado no provedor. Cada provedor externo que o aplicativo usa deve ser configurado de forma independente com a ID do cliente e o segredo do cliente do provedor. Para obter mais informações, confira os tópicos de provedor de autenticação externa que se aplicam:

As declarações opcionais enviadas na ID ou no token de acesso do provedor de autenticação geralmente são configuradas no portal online do provedor. Por exemplo, o Microsoft Entra ID permite atribuir declarações opcionais ao token de ID do aplicativo na folha Configuração de token do registro do aplicativo. Para obter mais informações, confira Como fornecer declarações opcionais para seu aplicativo (documentação do Azure). Para outros provedores, confira os conjuntos de documentação externos.

O aplicativo de exemplo configura o provedor de autenticação do Google com uma ID do cliente e um segredo do cliente fornecidos pelo 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.

Estabelecer o escopo de autenticação

Especifique a lista de permissões a serem recuperadas do provedor especificando o Scope. Os escopos de autenticação para provedores externos comuns aparecem na tabela a seguir.

Provedor Escopo
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

No aplicativo de exemplo, os escopos profile, email e openid do Google são adicionados automaticamente pela estrutura quando AddGoogle é chamado no AuthenticationBuilder. Se o aplicativo exigir escopos adicionais, adicione-os às opções. No exemplo a seguir, o escopo https://www.googleapis.com/auth/user.birthday.read do Google é adicionado para recuperar o aniversário de um usuário:

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

Mapear chaves de dados do usuário e criar declarações

Nas opções do provedor, especifique um MapJsonKey ou MapJsonSubKey para cada chave ou subchave nos dados do usuário JSON do provedor externo para que a identity do aplicativo seja lida no logon. Para obter mais informações sobre tipos de declarações, confira ClaimTypes.

O aplicativo de exemplo cria declarações de localidade (urn:google:locale) e imagem (urn:google:picture) das chaves locale e picture nos dados de usuário do Google:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

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

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

No Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, um IdentityUser (ApplicationUser) é conectado ao aplicativo com SignInAsync. Durante o processo de entrada, o UserManager<TUser> pode armazenar uma declaração ApplicationUser para os dados do usuário disponíveis no Principal.

No aplicativo de exemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) estabelece as declarações de localidade (urn:google:locale) e imagem (urn:google:picture) para o ApplicationUser conectado, incluindo uma declaração para 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();
}

Por padrão, as declarações de um usuário são armazenadas no cookie de autenticação. Se o cookie de autenticação for muito grande, isso poderá fazer com que o aplicativo falhe porque:

  • O navegador detecta que o cabeçalho do cookie é muito longo.
  • O tamanho geral da solicitação é muito grande.

Se uma grande quantidade de dados do usuário for necessária para processar solicitações do usuário:

  • Limite o número e o tamanho das declarações de usuário para apenas processar as solicitações que o aplicativo requer.
  • Use um ITicketStore personalizado para SessionStore do Middleware de Autenticação de Cookie armazenar a identity nas solicitações. Preserve grandes quantidades de informações de identity no servidor ao enviar apenas uma pequena chave de identificador de sessão para o cliente.

Salve o token de acesso

SaveTokens define se os tokens de acesso e atualização devem ser armazenados no AuthenticationProperties após uma autorização bem-sucedida. SaveTokens é definido como false por padrão para reduzir o tamanho da autenticação cookie final.

O aplicativo de exemplo define o valor de SaveTokens como true em 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;
    };
});

Quando OnPostConfirmationAsync for executado, armazene o token de acesso (ExternalLoginInfo.AuthenticationTokens) do provedor externo no ApplicationUser.AuthenticationProperties

O aplicativo de exemplo salva o token de acesso em OnPostConfirmationAsync (novo registro de usuário) e OnGetCallbackAsync (usuário registrado anteriormente) em 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();
}

Observação

Para obter informações sobre como passar tokens para os componentes Razor de um aplicativo Blazor do lado do servidor, consulte Cenários de segurança adicionais do ASP.NET Core Blazor do lado do servidor.

Como adicionar tokens personalizados adicionais

Para demonstrar como adicionar um token personalizado, que é armazenado como parte do SaveTokens, o aplicativo de exemplo adiciona um AuthenticationToken com o DateTime atual para um AuthenticationToken.Name de TicketCreated:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

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

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Criar e adicionar declarações

A estrutura fornece ações comuns e métodos de extensão para criar e adicionar declarações à coleção. Para obter mais informações, veja o ClaimActionCollectionMapExtensions e o ClaimActionCollectionUniqueExtensions.

Os usuários podem definir ações personalizadas derivando de ClaimAction e implementando o método abstrato Run.

Para obter mais informações, consulte Microsoft.AspNetCore.Authentication.OAuth.Claims.

Adicionar e atualizar declarações de usuário

As declarações são copiadas de provedores externos para o banco de dados do usuário no primeiro registro, não no login. Se declarações adicionais forem habilitadas em um aplicativo depois que um usuário se registrar para usar o aplicativo, chame SignInManager.RefreshSignInAsync em um usuário para forçar a geração de uma nova autenticação cookie.

No Ambiente de desenvolvimento que trabalha com as contas de usuário de teste, exclua e recrie a conta de usuário. Para sistemas de produção, novas declarações adicionadas ao aplicativo podem ser preenchidas novamente em contas de usuário. Depois de fazer scaffolding da página ExternalLogin no aplicativo em Areas/Pages/Identity/Account/Manage, adicione o código a seguir ao ExternalLoginModel no arquivo ExternalLogin.cshtml.cs.

Adicione um dicionário de declarações adicionadas. Use as chaves de dicionário para manter os tipos de declaração e use os valores para armazenar um valor padrão. Adicione a seguinte linha no topo da classe. O exemplo a seguir pressupõe que uma declaração seja adicionada à foto do Google do usuário com uma imagem de headshot genérica como o valor padrão:

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

Substitua o código padrão do método OnGetCallbackAsync pelo código a seguir. O código faz um loop pelo dicionário de declarações. As declarações são adicionadas (provisionadas) ou atualizadas para cada usuário. Quando as declarações são adicionadas ou atualizadas, a entrada do usuário é atualizada usando o SignInManager<TUser>, preservando as propriedades de autenticação existentes (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();
    }
}

Uma abordagem semelhante é adotada quando as declarações são alteradas enquanto um usuário está conectado, mas uma etapa de preenchimento não é necessária. Para atualizar as declarações de um usuário, chame o seguinte no usuário:

Remover ações de declaração e declarações

ClaimActionCollection.Remove(String) remove todas as ações de declaração para o ClaimType fornecido da coleção. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) exclui uma declaração do ClaimType a partir da identity. DeleteClaim é usado principalmente com o OIDC (OpenID Connect) para remover declarações geradas por protocolo.

Exemplo de saída do aplicativo

Execute o aplicativo de exemplo e selecione o link MyClaims :

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Encaminhar informações de solicitação com um proxy ou balanceador de carga

Se o aplicativo for implantado atrás de um servidor proxy ou um balanceador de carga, algumas das informações da solicitação original podem ser encaminhadas para o aplicativo nos cabeçalhos de solicitação. Essas informações geralmente incluem o esquema de solicitação segura (https), o host e o endereço IP do cliente. Os aplicativos não leem automaticamente esses cabeçalhos de solicitação para descobrir e usar as informações da solicitação original.

O esquema é usado na geração de link que afeta o fluxo de autenticação com provedores externos. Perder o esquema de seguro (https) resulta no aplicativo gerando URLs de redirecionamento inseguros incorretos.

Use Middleware de cabeçalhos encaminhados para disponibilizar as informações da solicitação original ao aplicativo para o processamento da solicitação.

Para obter mais informações, veja Configurar o ASP.NET Core para trabalhar com servidores proxy e balanceadores de carga.

Exibir ou baixar código de exemplo (como baixar)

Um aplicativo ASP.NET Core pode estabelecer declarações e tokens adicionais de provedores de autenticação externa, como Facebook, Google, Microsoft e Twitter. Cada provedor revela informações diferentes sobre os usuários em sua plataforma, mas o padrão para receber e transformar dados do usuário em declarações adicionais é o mesmo.

Exibir ou baixar código de exemplo (como baixar)

Pré-requisitos

Decida quais provedores de autenticação externa serão compatíveis com o aplicativo. Para cada provedor, registre o aplicativo e obtenha uma ID do cliente e um segredo do cliente. Para obter mais informações, confira Autenticação do Facebook e do Google no ASP.NET Core. O aplicativo de exemplo usa o provedor de autenticação do Google.

Defina a ID do cliente e o segredo do cliente

O provedor de autenticação OAuth estabelece uma relação de confiança com um aplicativo usando uma ID do cliente e um segredo do cliente. Os valores de ID do cliente e segredo do cliente são criados para o aplicativo pelo provedor de autenticação externa quando o aplicativo é registrado no provedor. Cada provedor externo que o aplicativo usa deve ser configurado de forma independente com a ID do cliente e o segredo do cliente do provedor. Para obter mais informações, confira os tópicos de provedor de autenticação externa que se aplicam ao seu cenário:

As declarações opcionais enviadas na ID ou no token de acesso do provedor de autenticação geralmente são configuradas no portal online do provedor. Por exemplo, o Microsoft Entra ID permite que você atribua declarações opcionais ao token de ID do aplicativo na folha Configuração de token do registro do aplicativo. Para obter mais informações, confira Como fornecer declarações opcionais para seu aplicativo (documentação do Azure). Para outros provedores, confira os conjuntos de documentação externos.

O aplicativo de exemplo configura o provedor de autenticação do Google com uma ID do cliente e um segredo do cliente fornecidos pelo 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;
    };
});

Estabelecer o escopo de autenticação

Especifique a lista de permissões a serem recuperadas do provedor especificando o Scope. Os escopos de autenticação para provedores externos comuns aparecem na tabela a seguir.

Provedor Escopo
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

No aplicativo de exemplo, os escopos profile, email e openid do Google são adicionados automaticamente pela estrutura quando AddGoogle é chamado no AuthenticationBuilder. Se o aplicativo exigir escopos adicionais, adicione-os às opções. No exemplo a seguir, o escopo https://www.googleapis.com/auth/user.birthday.read do Google é adicionado para recuperar o aniversário de um usuário:

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

Mapear chaves de dados do usuário e criar declarações

Nas opções do provedor, especifique um MapJsonKey ou MapJsonSubKey para cada chave/subchave nos dados do usuário JSON do provedor externo para que a identity do aplicativo seja lida no logon. Para obter mais informações sobre tipos de declarações, confira ClaimTypes.

O aplicativo de exemplo cria declarações de localidade (urn:google:locale) e imagem (urn:google:picture) das chaves locale e picture nos dados de usuário do 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;
    };
});

No Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, um IdentityUser (ApplicationUser) é conectado ao aplicativo com SignInAsync. Durante o processo de entrada, o UserManager<TUser> pode armazenar uma declaração ApplicationUser para os dados do usuário disponíveis no Principal.

No aplicativo de exemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) estabelece as declarações de localidade (urn:google:locale) e imagem (urn:google:picture) para o ApplicationUser conectado, incluindo uma declaração para 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();
}

Por padrão, as declarações de um usuário são armazenadas no cookie de autenticação. Se o cookie de autenticação for muito grande, isso poderá fazer com que o aplicativo falhe porque:

  • O navegador detecta que o cabeçalho do cookie é muito longo.
  • O tamanho geral da solicitação é muito grande.

Se uma grande quantidade de dados do usuário for necessária para processar solicitações do usuário:

  • Limite o número e o tamanho das declarações de usuário para apenas processar as solicitações que o aplicativo requer.
  • Use um ITicketStore personalizado para SessionStore do Middleware de Autenticação de Cookie armazenar a identity nas solicitações. Preserve grandes quantidades de informações de identity no servidor ao enviar apenas uma pequena chave de identificador de sessão para o cliente.

Salve o token de acesso

SaveTokens define se os tokens de acesso e atualização devem ser armazenados no AuthenticationProperties após uma autorização bem-sucedida. SaveTokens é definido como false por padrão para reduzir o tamanho da autenticação cookie final.

O aplicativo de exemplo define o valor de SaveTokens como true em 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;
    };
});

Quando OnPostConfirmationAsync for executado, armazene o token de acesso (ExternalLoginInfo.AuthenticationTokens) do provedor externo no ApplicationUser.AuthenticationProperties

O aplicativo de exemplo salva o token de acesso em OnPostConfirmationAsync (novo registro de usuário) e OnGetCallbackAsync (usuário registrado anteriormente) em 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();
}

Observação

Para obter informações sobre como passar tokens para os componentes Razor de um aplicativo Blazor do lado do servidor, consulte Cenários de segurança adicionais do ASP.NET Core Blazor do lado do servidor.

Como adicionar tokens personalizados adicionais

Para demonstrar como adicionar um token personalizado, que é armazenado como parte do SaveTokens, o aplicativo de exemplo adiciona um AuthenticationToken com o DateTime atual para um AuthenticationToken.Name de 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;
    };
});

Criando e adicionando declarações

A estrutura fornece ações comuns e métodos de extensão para criar e adicionar declarações à coleção. Para obter mais informações, veja o ClaimActionCollectionMapExtensions e o ClaimActionCollectionUniqueExtensions.

Os usuários podem definir ações personalizadas derivando de ClaimAction e implementando o método abstrato Run.

Para obter mais informações, consulte Microsoft.AspNetCore.Authentication.OAuth.Claims.

Adicionar e atualizar declarações de usuário

As declarações são copiadas de provedores externos para o banco de dados do usuário no primeiro registro, não no login. Se declarações adicionais forem habilitadas em um aplicativo depois que um usuário se registrar para usar o aplicativo, chame SignInManager.RefreshSignInAsync em um usuário para forçar a geração de uma nova autenticação cookie.

No Ambiente de desenvolvimento que trabalha com as contas de usuário de teste, basta excluir e recriar a conta de usuário. Para sistemas de produção, novas declarações adicionadas ao aplicativo podem ser preenchidas novamente em contas de usuário. Depois de fazer scaffolding da página ExternalLogin no aplicativo em Areas/Pages/Identity/Account/Manage, adicione o código a seguir ao ExternalLoginModel no arquivo ExternalLogin.cshtml.cs.

Adicione um dicionário de declarações adicionadas. Use as chaves de dicionário para manter os tipos de declaração e use os valores para armazenar um valor padrão. Adicione a seguinte linha no topo da classe. O exemplo a seguir pressupõe que uma declaração seja adicionada à foto do Google do usuário com uma imagem de headshot genérica como o valor padrão:

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

Substitua o código padrão do método OnGetCallbackAsync pelo código a seguir. O código faz um loop pelo dicionário de declarações. As declarações são adicionadas (provisionadas) ou atualizadas para cada usuário. Quando as declarações são adicionadas ou atualizadas, a entrada do usuário é atualizada usando o SignInManager<TUser>, preservando as propriedades de autenticação existentes (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();
    }
}

Uma abordagem semelhante é adotada quando as declarações são alteradas enquanto um usuário está conectado, mas uma etapa de preenchimento não é necessária. Para atualizar as declarações de um usuário, chame o seguinte no usuário:

Remoção de ações de declaração e declarações

ClaimActionCollection.Remove(String) remove todas as ações de declaração para o ClaimType fornecido da coleção. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) exclui uma declaração do ClaimType a partir da identity. DeleteClaim é usado principalmente com o OIDC (OpenID Connect) para remover declarações geradas por protocolo.

Exemplo de saída do aplicativo

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

Encaminhar informações de solicitação com um proxy ou balanceador de carga

Se o aplicativo for implantado atrás de um servidor proxy ou um balanceador de carga, algumas das informações da solicitação original podem ser encaminhadas para o aplicativo nos cabeçalhos de solicitação. Essas informações geralmente incluem o esquema de solicitação segura (https), o host e o endereço IP do cliente. Os aplicativos não leem automaticamente esses cabeçalhos de solicitação para descobrir e usar as informações da solicitação original.

O esquema é usado na geração de link que afeta o fluxo de autenticação com provedores externos. Perder o esquema de seguro (https) resulta no aplicativo gerando URLs de redirecionamento inseguros incorretos.

Use Middleware de cabeçalhos encaminhados para disponibilizar as informações da solicitação original ao aplicativo para o processamento da solicitação.

Para obter mais informações, veja Configurar o ASP.NET Core para trabalhar com servidores proxy e balanceadores de carga.

Um aplicativo ASP.NET Core pode estabelecer declarações e tokens adicionais de provedores de autenticação externa, como Facebook, Google, Microsoft e Twitter. Cada provedor revela informações diferentes sobre os usuários em sua plataforma, mas o padrão para receber e transformar dados do usuário em declarações adicionais é o mesmo.

Exibir ou baixar código de exemplo (como baixar)

Pré-requisitos

Decida quais provedores de autenticação externa serão compatíveis com o aplicativo. Para cada provedor, registre o aplicativo e obtenha uma ID do cliente e um segredo do cliente. Para obter mais informações, confira Autenticação do Facebook e do Google no ASP.NET Core. O aplicativo de exemplo usa o provedor de autenticação do Google.

Defina a ID do cliente e o segredo do cliente

O provedor de autenticação OAuth estabelece uma relação de confiança com um aplicativo usando uma ID do cliente e um segredo do cliente. Os valores de ID do cliente e segredo do cliente são criados para o aplicativo pelo provedor de autenticação externa quando o aplicativo é registrado no provedor. Cada provedor externo que o aplicativo usa deve ser configurado de forma independente com a ID do cliente e o segredo do cliente do provedor. Para obter mais informações, confira os tópicos de provedor de autenticação externa que se aplicam ao seu cenário:

O aplicativo de exemplo configura o provedor de autenticação do Google com uma ID do cliente e um segredo do cliente fornecidos pelo 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;
    };
});

Estabelecer o escopo de autenticação

Especifique a lista de permissões a serem recuperadas do provedor especificando o Scope. Os escopos de autenticação para provedores externos comuns aparecem na tabela a seguir.

Provedor Escopo
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

No aplicativo de exemplo, o escopo userinfo.profile do Google é adicionado automaticamente pela estrutura quando AddGoogle é chamado no AuthenticationBuilder. Se o aplicativo exigir escopos adicionais, adicione-os às opções. No exemplo a seguir, o escopo https://www.googleapis.com/auth/user.birthday.read do Google é adicionado a fim de recuperar o aniversário de um usuário:

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

Mapear chaves de dados do usuário e criar declarações

Nas opções do provedor, especifique um MapJsonKey ou MapJsonSubKey para cada chave/subchave nos dados do usuário JSON do provedor externo para que a identity do aplicativo seja lida no logon. Para obter mais informações sobre tipos de declarações, confira ClaimTypes.

O aplicativo de exemplo cria declarações de localidade (urn:google:locale) e imagem (urn:google:picture) das chaves locale e picture nos dados de usuário do 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;
    };
});

No Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, um IdentityUser (ApplicationUser) é conectado ao aplicativo com SignInAsync. Durante o processo de entrada, o UserManager<TUser> pode armazenar uma declaração ApplicationUser para os dados do usuário disponíveis no Principal.

No aplicativo de exemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) estabelece as declarações de localidade (urn:google:locale) e imagem (urn:google:picture) para o ApplicationUser conectado, incluindo uma declaração para 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();
}

Por padrão, as declarações de um usuário são armazenadas no cookie de autenticação. Se o cookie de autenticação for muito grande, isso poderá fazer com que o aplicativo falhe porque:

  • O navegador detecta que o cabeçalho do cookie é muito longo.
  • O tamanho geral da solicitação é muito grande.

Se uma grande quantidade de dados do usuário for necessária para processar solicitações do usuário:

  • Limite o número e o tamanho das declarações de usuário para apenas processar as solicitações que o aplicativo requer.
  • Use um ITicketStore personalizado para SessionStore do Middleware de Autenticação de Cookie armazenar a identity nas solicitações. Preserve grandes quantidades de informações de identity no servidor ao enviar apenas uma pequena chave de identificador de sessão para o cliente.

Salve o token de acesso

SaveTokens define se os tokens de acesso e atualização devem ser armazenados no AuthenticationProperties após uma autorização bem-sucedida. SaveTokens é definido como false por padrão para reduzir o tamanho da autenticação cookie final.

O aplicativo de exemplo define o valor de SaveTokens como true em 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;
    };
});

Quando OnPostConfirmationAsync for executado, armazene o token de acesso (ExternalLoginInfo.AuthenticationTokens) do provedor externo no ApplicationUser.AuthenticationProperties

O aplicativo de exemplo salva o token de acesso em OnPostConfirmationAsync (novo registro de usuário) e OnGetCallbackAsync (usuário registrado anteriormente) em 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();
}

Como adicionar tokens personalizados adicionais

Para demonstrar como adicionar um token personalizado, que é armazenado como parte do SaveTokens, o aplicativo de exemplo adiciona um AuthenticationToken com o DateTime atual para um AuthenticationToken.Name de 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;
    };
});

Criando e adicionando declarações

A estrutura fornece ações comuns e métodos de extensão para criar e adicionar declarações à coleção. Para obter mais informações, veja o ClaimActionCollectionMapExtensions e o ClaimActionCollectionUniqueExtensions.

Os usuários podem definir ações personalizadas derivando de ClaimAction e implementando o método abstrato Run.

Para obter mais informações, consulte Microsoft.AspNetCore.Authentication.OAuth.Claims.

Remoção de ações de declaração e declarações

ClaimActionCollection.Remove(String) remove todas as ações de declaração para o ClaimType fornecido da coleção. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) exclui uma declaração do ClaimType a partir da identity. DeleteClaim é usado principalmente com o OIDC (OpenID Connect) para remover declarações geradas por protocolo.

Exemplo de saída do aplicativo

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

Encaminhar informações de solicitação com um proxy ou balanceador de carga

Se o aplicativo for implantado atrás de um servidor proxy ou um balanceador de carga, algumas das informações da solicitação original podem ser encaminhadas para o aplicativo nos cabeçalhos de solicitação. Essas informações geralmente incluem o esquema de solicitação segura (https), o host e o endereço IP do cliente. Os aplicativos não leem automaticamente esses cabeçalhos de solicitação para descobrir e usar as informações da solicitação original.

O esquema é usado na geração de link que afeta o fluxo de autenticação com provedores externos. Perder o esquema de seguro (https) resulta no aplicativo gerando URLs de redirecionamento inseguros incorretos.

Use Middleware de cabeçalhos encaminhados para disponibilizar as informações da solicitação original ao aplicativo para o processamento da solicitação.

Para obter mais informações, veja Configurar o ASP.NET Core para trabalhar com servidores proxy e balanceadores de carga.

Recursos adicionais