Compartir vía


Conservar notificaciones y tokens adicionales de proveedores externos en ASP.NET Core

Una aplicación ASP.NET Core puede establecer notificaciones y tokens adicionales de proveedores de autenticación externos, como Facebook, Google, Microsoft y Twitter. Cada proveedor revela información diferente sobre los usuarios en su plataforma, pero el patrón para recibir y transformar datos de usuario en notificaciones adicionales es el mismo.

Prerrequisitos

Decida qué proveedores de autenticación externos admitir en la aplicación. Para cada proveedor, registre la aplicación y obtenga un identificador de cliente y un secreto de cliente. Para obtener más información, consulte Autenticación de Facebook y Google en ASP.NET Core. La aplicación de ejemplo usa el proveedor de autenticación de Google.

Copia el identificador de cliente y el secreto de cliente

El proveedor de autenticación OAuth establece una relación de confianza con una aplicación mediante un identificador de cliente y un secreto de cliente. El proveedor de autenticación externo crea los valores de identificación de cliente y secreto de cliente para la aplicación cuando esta está registrada con el proveedor. Cada proveedor externo que use la aplicación debe configurarse de forma independiente con el identificador de cliente y el secreto de cliente del proveedor. Para obtener más información, consulte los temas del proveedor de autenticación externo que se aplican:

Las notificaciones opcionales enviadas en el identificador o el token de acceso desde el proveedor de autenticación normalmente se configuran en el portal en línea del proveedor. Por ejemplo, Microsoft Entra ID permite asignar reclamaciones opcionales al token de id. de la aplicación en la hoja de configuración de Token del registro de la aplicación. Para más información, consulte Cómo: Proporcionar notificaciones opcionales a la aplicación (documentación de Azure). Para otros proveedores, consulte sus conjuntos de documentación externos.

La aplicación de ejemplo configura el proveedor de autenticación de Google con un identificador de cliente y un secreto de cliente proporcionados por 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.

Establece del ámbito de autenticación

Especifica la lista de permisos que se van a recuperar del proveedor especificando Scope. Los ámbitos de autenticación para proveedores externos comunes aparecen en la tabla siguiente.

Proveedor Ámbito
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

En la aplicación de ejemplo, el marco agrega automáticamente los ámbitos profile, email y openid de Google cuando AddGoogle se llama a en AuthenticationBuilder. Si la aplicación requiere ámbitos adicionales, agréguelos a las opciones. En el ejemplo siguiente, se agrega el ámbito de Google https://www.googleapis.com/auth/user.birthday.read para recuperar el cumpleaños de un usuario:

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

Asignación de claves de datos de usuario y creación de notificaciones

En las opciones del proveedor, especifica MapJsonKey o MapJsonSubKey para cada clave o subclave en los datos de usuario JSON del proveedor externo para que la identity de la aplicación lea el inicio de sesión. Para obtener más información sobre los tipos de notificaciones, consulte ClaimTypes.

La aplicación de ejemplo crea notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) a partir de las claves locale y picture en los datos de usuario de 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;
    };
});

En Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, una IdentityUser (ApplicationUser) ha iniciado sesión en la aplicación con SignInAsync. Durante el proceso de inicio de sesión, UserManager<TUser> puede almacenar una notificación ApplicationUser para los datos de usuario disponibles en Principal.

En la aplicación de ejemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establece las notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) para el que ha iniciado sesión ApplicationUser, incluida una notificación 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();
}

De forma predeterminada, las notificaciones de un usuario se almacenan en la autenticación cookie. Si la autenticación cookie es demasiado grande, puede hacer que se produzca un error en la aplicación porque:

  • El explorador detecta que el encabezado cookie es demasiado largo.
  • El tamaño general de la solicitud es demasiado grande.

Si se requiere una gran cantidad de datos de usuario para procesar solicitudes de usuario:

  • Limite el número y el tamaño de las notificaciones de usuario para el procesamiento de solicitudes solo a lo que requiere la aplicación.
  • Usa un elemento ITicketStore personalizado para el middleware de autenticación de Cookie de SessionStore para almacenar la identity entre las solicitudes. Conserva grandes cantidades de información de identity en el servidor mientras solo envías una clave de identificador de sesión pequeña al cliente.

Guarde el token de acceso

SaveTokens define si los tokens de acceso y actualización deben almacenarse en AuthenticationProperties después de una autorización correcta. SaveTokens se establece false en de forma predeterminada para reducir el tamaño de la autenticación final cookie.

La aplicación de ejemplo establece el valor de SaveTokens en true en 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;
    };
});

Cuando OnPostConfirmationAsync se ejecuta, almacene el token de acceso (ExternalLoginInfo.AuthenticationTokens) desde el proveedor externo en el AuthenticationProperties de ApplicationUser.

La aplicación de ejemplo guarda el token de acceso en OnPostConfirmationAsync (nuevo registro de usuario) y OnGetCallbackAsync (usuario registrado previamente) en 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();
}

Nota:

Para obtener información sobre cómo pasar tokens a los Razor componentes de una aplicación Blazor del lado del servidor, consulte los escenarios de seguridad adicionales de Blazor de ASP.NET Core del lado del servidor.

Como añadir tokens personalizados adicionales

Para demostrar cómo agregar un token personalizado, que se almacena como parte de SaveTokens, la aplicación de ejemplo agrega un AuthenticationToken con el actual DateTime para un 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;
    };
});

Crear y añadir notificaciones

El marco proporciona acciones comunes y métodos de extensión para crear y agregar notificaciones a la colección. Para obtener más información, vea ClaimActionCollectionMapExtensions y ClaimActionCollectionUniqueExtensions.

Los usuarios pueden definir acciones personalizadas derivando de ClaimAction e implementando el método abstracto Run.

Para obtener más información, vea Microsoft.AspNetCore.Authentication.OAuth.Claims.

Añadir y actualizar las notificaciones de usuario

Las notificaciones se copian de los proveedores externos a la base de datos de usuario en el primer registro, no en el inicio de sesión. Si se habilitan notificaciones adicionales en una aplicación después de que un usuario se registre para usar la aplicación, llame a SignInManager.RefreshSignInAsync en un usuario para forzar la generación de una nueva autenticación cookie.

En el entorno de desarrollo que trabaja con cuentas de usuario de prueba, elimine y vuelva a crear la cuenta de usuario. En el caso de los sistemas de producción, las nuevas notificaciones agregadas a la aplicación se pueden volver a rellenar en las cuentas de usuario. Después de aplicar scaffolding a la página ExternalLogin en la aplicación en Areas/Pages/Identity/Account/Manage, agregue el código siguiente al ExternalLoginModel en el archivo ExternalLogin.cshtml.cs.

Agregue un diccionario de notificaciones agregadas. Use las claves de diccionario para contener los tipos de notificación y use los valores para contener un valor predeterminado. Agregue la siguiente línea al comienzo de la clase. En el ejemplo siguiente se supone que se agrega una notificación para la imagen de Google del usuario con una imagen genérica de una captura de pantalla como valor predeterminado:

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

Reemplace código predeterminado del método OnGetCallbackAsync por el siguiente código. El código recorre en bucle el diccionario de notificaciones. Las notificaciones se agregan (rellenadas) o se actualizan para cada usuario. Cuando se agregan o actualizan las notificaciones, el inicio de sesión del usuario se actualiza mediante SignInManager<TUser>, conservando las propiedades de autenticación 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();
    }
}

Se adopta un enfoque similar cuando las notificaciones cambian mientras un usuario ha iniciado sesión, pero no se requiere una etapa de reposición. Para actualizar las notificaciones de un usuario, llame a lo siguiente en el usuario:

Eliminar las notificaciones y las acciones de notificación

ClaimActionCollection.Remove(String) elimina todas las acciones de notificación para el ClaimType especificado de la colección. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) elimina una notificación del elemento ClaimType especificado de la identity. DeleteClaim se usa principalmente con OpenID Connect (OIDC) para eliminar las notificaciones generadas por protocolo.

Salida de la aplicación de ejemplo

Ejecute la aplicación de ejemplo y seleccione el vínculo 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

Reenvío de información de solicitud con un servidor proxy o un equilibrador de carga

Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta información incluye el esquema de solicitud seguro (https), el host y la dirección IP del cliente. Las aplicaciones no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.

El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El resultado de perder el esquema seguro (https) es que la aplicación genera direcciones URL incorrectas poco seguras.

Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la aplicación para procesar las solicitudes.

Para más información, vea Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Vea o descargue el código de ejemplo (cómo descargarlo)

Una aplicación ASP.NET Core puede establecer notificaciones y tokens adicionales de proveedores de autenticación externos, como Facebook, Google, Microsoft y Twitter. Cada proveedor revela información diferente sobre los usuarios en su plataforma, pero el patrón para recibir y transformar datos de usuario en notificaciones adicionales es el mismo.

Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos

Decida qué proveedores de autenticación externos admitir en la aplicación. Para cada proveedor, registre la aplicación y obtenga un identificador de cliente y un secreto de cliente. Para obtener más información, consulte Autenticación de Facebook y Google en ASP.NET Core. La aplicación de ejemplo usa el proveedor de autenticación de Google.

Copia el identificador de cliente y el secreto de cliente

El proveedor de autenticación OAuth establece una relación de confianza con una aplicación mediante un identificador de cliente y un secreto de cliente. El proveedor de autenticación externo crea los valores de identificación de cliente y secreto de cliente para la aplicación cuando esta está registrada con el proveedor. Cada proveedor externo que use la aplicación debe configurarse de forma independiente con el identificador de cliente y el secreto de cliente del proveedor. Para obtener más información, consulte los temas del proveedor de autenticación externo que se aplican a su escenario:

Las notificaciones opcionales enviadas en el identificador o el token de acceso desde el proveedor de autenticación normalmente se configuran en el portal en línea del proveedor. Por ejemplo, Microsoft Entra ID permite asignar reclamaciones opcionales al token de id de la aplicación en la hoja de configuración de Token del registro de la aplicación. Para más información, consulte Cómo: Proporcionar notificaciones opcionales a la aplicación (documentación de Azure). Para otros proveedores, consulte sus conjuntos de documentación externos.

La aplicación de ejemplo configura el proveedor de autenticación de Google con un identificador de cliente y un secreto de cliente proporcionados por 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;
    };
});

Establece del ámbito de autenticación

Especifica la lista de permisos que se van a recuperar del proveedor especificando Scope. Los ámbitos de autenticación para proveedores externos comunes aparecen en la tabla siguiente.

Proveedor Ámbito
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

En la aplicación de ejemplo, el marco agrega automáticamente los ámbitos profile, email y openid de Google cuando AddGoogle se llama a en AuthenticationBuilder. Si la aplicación requiere ámbitos adicionales, agréguelos a las opciones. En el ejemplo siguiente, se agrega el ámbito de Google https://www.googleapis.com/auth/user.birthday.read para recuperar el cumpleaños de un usuario:

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

Asignación de claves de datos de usuario y creación de notificaciones

En las opciones del proveedor, especifica MapJsonKey o MapJsonSubKey para cada clave o subclave en los datos de usuario JSON del proveedor externo para que la identity de la aplicación lea el inicio de sesión. Para obtener más información sobre los tipos de notificaciones, consulte ClaimTypes.

La aplicación de ejemplo crea notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) a partir de las claves locale y picture en los datos de usuario de 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;
    };
});

En Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, una IdentityUser (ApplicationUser) ha iniciado sesión en la aplicación con SignInAsync. Durante el proceso de inicio de sesión, UserManager<TUser> puede almacenar una notificación ApplicationUser para los datos de usuario disponibles en Principal.

En la aplicación de ejemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establece las notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) para el que ha iniciado sesión ApplicationUser, incluida una notificación 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();
}

De forma predeterminada, las notificaciones de un usuario se almacenan en la autenticación cookie. Si la autenticación cookie es demasiado grande, puede hacer que se produzca un error en la aplicación porque:

  • El explorador detecta que el encabezado cookie es demasiado largo.
  • El tamaño general de la solicitud es demasiado grande.

Si se requiere una gran cantidad de datos de usuario para procesar solicitudes de usuario:

  • Limite el número y el tamaño de las notificaciones de usuario para el procesamiento de solicitudes solo a lo que requiere la aplicación.
  • Usa un elemento ITicketStore personalizado para el middleware de autenticación de Cookie de SessionStore para almacenar la identity entre las solicitudes. Conserva grandes cantidades de información de identity en el servidor mientras solo envías una clave de identificador de sesión pequeña al cliente.

Guarde el token de acceso

SaveTokens define si los tokens de acceso y actualización deben almacenarse en AuthenticationProperties después de una autorización correcta. SaveTokens se establece false en de forma predeterminada para reducir el tamaño de la autenticación final cookie.

La aplicación de ejemplo establece el valor de SaveTokens en true en 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;
    };
});

Cuando OnPostConfirmationAsync se ejecuta, almacene el token de acceso (ExternalLoginInfo.AuthenticationTokens) desde el proveedor externo en el AuthenticationProperties de ApplicationUser.

La aplicación de ejemplo guarda el token de acceso en OnPostConfirmationAsync (nuevo registro de usuario) y OnGetCallbackAsync (usuario registrado previamente) en 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();
}

Nota:

Para obtener información sobre cómo pasar tokens a los Razor componentes de una aplicación Blazor del lado del servidor, consulte los escenarios de seguridad adicionales de Blazor de ASP.NET Core del lado del servidor.

Como añadir tokens personalizados adicionales

Para demostrar cómo agregar un token personalizado, que se almacena como parte de SaveTokens, la aplicación de ejemplo agrega un AuthenticationToken con el actual DateTime para un 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;
    };
});

Crear y añadir notificaciones

El marco proporciona acciones comunes y métodos de extensión para crear y agregar notificaciones a la colección. Para obtener más información, vea ClaimActionCollectionMapExtensions y ClaimActionCollectionUniqueExtensions.

Los usuarios pueden definir acciones personalizadas derivando de ClaimAction e implementando el método abstracto Run.

Para obtener más información, vea Microsoft.AspNetCore.Authentication.OAuth.Claims.

Añadir y actualizar las notificaciones de usuario

Las notificaciones se copian de los proveedores externos a la base de datos de usuario en el primer registro, no en el inicio de sesión. Si se habilitan notificaciones adicionales en una aplicación después de que un usuario se registre para usar la aplicación, llame a SignInManager.RefreshSignInAsync en un usuario para forzar la generación de una nueva autenticación cookie.

En el entorno de desarrollo que funciona con cuentas de usuario de prueba, puede simplemente eliminar y volver a crear la cuenta de usuario. En el caso de los sistemas de producción, las nuevas notificaciones agregadas a la aplicación se pueden volver a rellenar en las cuentas de usuario. Después de aplicar scaffolding a la página ExternalLogin en la aplicación en Areas/Pages/Identity/Account/Manage, agregue el código siguiente al ExternalLoginModel en el archivo ExternalLogin.cshtml.cs.

Agregue un diccionario de notificaciones agregadas. Use las claves de diccionario para contener los tipos de notificación y use los valores para contener un valor predeterminado. Agregue la siguiente línea al comienzo de la clase. En el ejemplo siguiente se supone que se agrega una notificación para la imagen de Google del usuario con una imagen genérica de una captura de pantalla como valor predeterminado:

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

Reemplace código predeterminado del método OnGetCallbackAsync por el siguiente código. El código recorre en bucle el diccionario de notificaciones. Las notificaciones se agregan (rellenadas) o se actualizan para cada usuario. Cuando se agregan o actualizan las notificaciones, el inicio de sesión del usuario se actualiza mediante SignInManager<TUser>, conservando las propiedades de autenticación 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();
    }
}

Se adopta un enfoque similar cuando las notificaciones cambian mientras un usuario ha iniciado sesión, pero no se requiere una etapa de reposición. Para actualizar las notificaciones de un usuario, llame a lo siguiente en el usuario:

Eliminación de notificaciones y acciones de notificación

ClaimActionCollection.Remove(String) elimina todas las acciones de notificación para el ClaimType especificado de la colección. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) elimina una notificación del elemento ClaimType especificado de la identity. DeleteClaim se usa principalmente con OpenID Connect (OIDC) para eliminar las notificaciones generadas por protocolo.

Salida de la aplicación de ejemplo

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

Reenvío de información de solicitud con un servidor proxy o un equilibrador de carga

Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta información incluye el esquema de solicitud seguro (https), el host y la dirección IP del cliente. Las aplicaciones no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.

El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El resultado de perder el esquema seguro (https) es que la aplicación genera direcciones URL incorrectas poco seguras.

Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la aplicación para procesar las solicitudes.

Para más información, vea Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Una aplicación ASP.NET Core puede establecer notificaciones y tokens adicionales de proveedores de autenticación externos, como Facebook, Google, Microsoft y Twitter. Cada proveedor revela información diferente sobre los usuarios en su plataforma, pero el patrón para recibir y transformar datos de usuario en notificaciones adicionales es el mismo.

Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos

Decida qué proveedores de autenticación externos admitir en la aplicación. Para cada proveedor, registre la aplicación y obtenga un identificador de cliente y un secreto de cliente. Para obtener más información, consulte Autenticación de Facebook y Google en ASP.NET Core. La aplicación de ejemplo usa el proveedor de autenticación de Google.

Copia el identificador de cliente y el secreto de cliente

El proveedor de autenticación OAuth establece una relación de confianza con una aplicación mediante un identificador de cliente y un secreto de cliente. El proveedor de autenticación externo crea los valores de identificación de cliente y secreto de cliente para la aplicación cuando esta está registrada con el proveedor. Cada proveedor externo que use la aplicación debe configurarse de forma independiente con el identificador de cliente y el secreto de cliente del proveedor. Para obtener más información, consulte los temas del proveedor de autenticación externo que se aplican a su escenario:

La aplicación de ejemplo configura el proveedor de autenticación de Google con un identificador de cliente y un secreto de cliente proporcionados por 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;
    };
});

Establece del ámbito de autenticación

Especifica la lista de permisos que se van a recuperar del proveedor especificando Scope. Los ámbitos de autenticación para proveedores externos comunes aparecen en la tabla siguiente.

Proveedor Ámbito
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

En la aplicación de ejemplo, el marco agrega automáticamente el ámbito userinfo.profile de Google cuando AddGoogle se llama en el AuthenticationBuilder. Si la aplicación requiere ámbitos adicionales, agréguelos a las opciones. En el ejemplo siguiente, se agrega el ámbito de Google https://www.googleapis.com/auth/user.birthday.read para recuperar el cumpleaños de un usuario:

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

Asignación de claves de datos de usuario y creación de notificaciones

En las opciones del proveedor, especifica MapJsonKey o MapJsonSubKey para cada clave o subclave en los datos de usuario JSON del proveedor externo para que la identity de la aplicación lea el inicio de sesión. Para obtener más información sobre los tipos de notificaciones, consulte ClaimTypes.

La aplicación de ejemplo crea notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) a partir de las claves locale y picture en los datos de usuario de 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;
    };
});

En Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, una IdentityUser (ApplicationUser) ha iniciado sesión en la aplicación con SignInAsync. Durante el proceso de inicio de sesión, UserManager<TUser> puede almacenar una notificación ApplicationUser para los datos de usuario disponibles en Principal.

En la aplicación de ejemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establece las notificaciones de configuración regional (urn:google:locale) e imagen (urn:google:picture) para el que ha iniciado sesión ApplicationUser, incluida una notificación 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();
}

De forma predeterminada, las notificaciones de un usuario se almacenan en la autenticación cookie. Si la autenticación cookie es demasiado grande, puede hacer que se produzca un error en la aplicación porque:

  • El explorador detecta que el encabezado cookie es demasiado largo.
  • El tamaño general de la solicitud es demasiado grande.

Si se requiere una gran cantidad de datos de usuario para procesar solicitudes de usuario:

  • Limite el número y el tamaño de las notificaciones de usuario para el procesamiento de solicitudes solo a lo que requiere la aplicación.
  • Usa un elemento ITicketStore personalizado para el middleware de autenticación de Cookie de SessionStore para almacenar la identity entre las solicitudes. Conserva grandes cantidades de información de identity en el servidor mientras solo envías una clave de identificador de sesión pequeña al cliente.

Guarde el token de acceso

SaveTokens define si los tokens de acceso y actualización deben almacenarse en AuthenticationProperties después de una autorización correcta. SaveTokens se establece false en de forma predeterminada para reducir el tamaño de la autenticación final cookie.

La aplicación de ejemplo establece el valor de SaveTokens en true en 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;
    };
});

Cuando OnPostConfirmationAsync se ejecuta, almacene el token de acceso (ExternalLoginInfo.AuthenticationTokens) desde el proveedor externo en el AuthenticationProperties de ApplicationUser.

La aplicación de ejemplo guarda el token de acceso en OnPostConfirmationAsync (nuevo registro de usuario) y OnGetCallbackAsync (usuario registrado previamente) en 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 añadir tokens personalizados adicionales

Para demostrar cómo agregar un token personalizado, que se almacena como parte de SaveTokens, la aplicación de ejemplo agrega un AuthenticationToken con el actual DateTime para un 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;
    };
});

Crear y añadir notificaciones

El marco proporciona acciones comunes y métodos de extensión para crear y agregar notificaciones a la colección. Para obtener más información, vea ClaimActionCollectionMapExtensions y ClaimActionCollectionUniqueExtensions.

Los usuarios pueden definir acciones personalizadas derivando de ClaimAction e implementando el método abstracto Run.

Para obtener más información, vea Microsoft.AspNetCore.Authentication.OAuth.Claims.

Eliminación de notificaciones y acciones de notificación

ClaimActionCollection.Remove(String) elimina todas las acciones de notificación para el ClaimType especificado de la colección. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) elimina una notificación del elemento ClaimType especificado de la identity. DeleteClaim se usa principalmente con OpenID Connect (OIDC) para eliminar las notificaciones generadas por protocolo.

Salida de la aplicación de ejemplo

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

Reenvío de información de solicitud con un servidor proxy o un equilibrador de carga

Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta información incluye el esquema de solicitud seguro (https), el host y la dirección IP del cliente. Las aplicaciones no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.

El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El resultado de perder el esquema seguro (https) es que la aplicación genera direcciones URL incorrectas poco seguras.

Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la aplicación para procesar las solicitudes.

Para más información, vea Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Recursos adicionales

  • Aplicación de ingeniería SocialSample dotnet/AspNetCore: la aplicación de ejemplo vinculada está en la rama de ingeniería del repositorio de GitHub main dotnet/AspNetCore. La rama main contiene un código en desarrollo activo para la próxima versión de ASP.NET Core. Para ver una versión de la aplicación de ejemplo para una versión publicada de ASP.NET Core, use la lista desplegable Rama para seleccionar una rama de versión (por ejemplo release/{X.Y}).