Rendre persistants les revendications et les jetons supplémentaires provenant de fournisseurs externes dans ASP.NET Core

Une application ASP.NET Core peut établir des revendications et des jetons supplémentaires à partir de fournisseurs d’authentification externes, tels que Facebook, Google, Microsoft et Twitter. Chaque fournisseur révèle des informations différentes concernant les utilisateurs sur sa plateforme, mais le modèle de réception et de transformation des données utilisateur en revendications supplémentaires reste le même.

Prérequis

Déterminez les fournisseurs d’authentification externes à prendre en charge dans l’application. Pour chaque fournisseur, inscrivez l’application et obtenez un ID client et une clé secrète client. Pour plus d’informations, consultez Authentification Facebook et Google dans ASP.NET Core. L’exemple d’application utilise le fournisseur d’authentification Google.

Définir l’ID client et le secret client

Le fournisseur d’authentification OAuth établit une relation d’approbation avec une application à l’aide d’un ID client et d’une clé secrète client. Les valeurs d’ID client et de clé secrète client sont créées pour l’application par le fournisseur d’authentification externe lorsque l’application est inscrite auprès du fournisseur. Chaque fournisseur externe utilisé par l’application doit être configuré indépendamment avec l’ID client et la clé secrète client du fournisseur. Pour plus d’informations, consultez les rubriques du fournisseur d’authentification externe qui s’appliquent :

Les revendications facultatives envoyées dans l’ID ou le jeton d’accès du fournisseur d’authentification sont généralement configurées dans le portail en ligne du fournisseur. Par exemple, Microsoft Azure Active Directory (AAD) permet d’attribuer des revendications facultatives au jeton d’ID de l’application dans le panneau Configuration du jeton de l’inscription de l’application. Pour plus d’informations, consultez Guide pratique pour fournir des revendications facultatives à votre application (documentation Azure). Pour les autres fournisseurs, consultez leurs ensembles de documentation externes.

L’exemple d’application configure le fournisseur d’authentification Google avec un ID client et une clé secrète client fournis par 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.

Établir l’étendue de l’authentification

Définissez la liste des autorisations à récupérer à partir du fournisseur en spécifiant Scope. Les étendues d’authentification pour les fournisseurs externes courants apparaissent dans le tableau suivant.

Fournisseur Portée
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

Dans l’exemple d’application, les étendues profile, email et openid de Google sont automatiquement ajoutées par l’infrastructure quand AddGoogle est appelée sur AuthenticationBuilder. Si l’application nécessite des étendues supplémentaires, ajoutez-les aux options. Dans l’exemple suivant, l’étendue Google https://www.googleapis.com/auth/user.birthday.read est ajoutée pour récupérer l’anniversaire d’un utilisateur :

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

Mapper les clés de données utilisateur et créer des revendications

Dans les options du fournisseur, spécifiez un MapJsonKey ou MapJsonSubKey pour chaque clé ou sous-clé dans le JS du fournisseur externe SUR les données utilisateur pour que l’identité de l’application soit lue lors de la connexion. Pour plus d’informations sur les nouveaux types de revendications, consultez ClaimTypes.

L’exemple d’application crée des revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) à partir des clés locale et picture dans les données utilisateur 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;
    };
});

Dans Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, un IdentityUser (ApplicationUser) est connecté à l’application avec SignInAsync. Pendant le processus de connexion, UserManager<TUser> peut stocker des revendications ApplicationUser pour les données utilisateur disponibles à partir de Principal.

Dans l’exemple d’application, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) établit les revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) pour l’ApplicationUser connecté, y compris une revendication pour 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();
}

Par défaut, les revendications d’un utilisateur sont stockées dans l’authentification cookie. Si l’authentification cookie est trop grande, cela peut entraîner l’échec de l’application pour les raisons suivantes :

  • Le navigateur détecte que l’en-tête cookie est trop long.
  • La taille globale de la requête est trop grande.

Si une grande quantité de données utilisateur est requise pour le traitement des demandes des utilisateurs :

  • Limitez le nombre et la taille des revendications utilisateur pour le traitement des demandes uniquement à ce dont l’application a besoin.
  • Utilisez un logiciel personnalisé ITicketStore pour le Cookie de l’intergiciel d’authentification pour stocker l’identité SessionStore entre les requêtes. Conservez de grandes quantités d’informations d’identité sur le serveur tout en envoyant une petite clé d’identificateur de session au client.

Enregistrer le jeton d’accès

SaveTokens définit si les jetons d’accès et d’actualisation doivent être stockés dans AuthenticationProperties après une autorisation réussie. SaveTokens est défini sur false par défaut pour réduire la taille de l’authentification finale cookie.

L’exemple d’application définit la valeur de SaveTokens sur true dans 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;
    };
});

Lors de l’exécution de OnPostConfirmationAsync, stockez le jeton d’accès (ExternalLoginInfo.AuthenticationTokens) à partir du fournisseur externe dans le AuthenticationProperties de ApplicationUser.

L’exemple d’application enregistre le jeton d’accès dans OnPostConfirmationAsync (nouvelle inscription de l’utilisateur) et OnGetCallbackAsync (utilisateur précédemment inscrit) dans 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();
}

Remarque

Pour plus d’informations sur la transmission de jetons aux composants Razor d’une application Blazor côté serveur, consultez les scénarios de sécurité supplémentaire de ASP.NET Core côté serveurBlazor.

Comment ajouter des jetons personnalisés supplémentaires

Pour montrer comment ajouter un jeton personnalisé, qui est stocké dans le cadre de SaveTokens, l’exemple d’application ajoute un AuthenticationToken avec le DateTime actuel pour 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;
    };
});

Créer et ajouter des revendications

L’infrastructure fournit des actions courantes et des méthodes d’extension pour créer et ajouter des revendications à la collection. Pour plus d’informations, consultez ClaimActionCollectionMapExtensions et ClaimActionCollectionUniqueExtensions.

Les utilisateurs peuvent définir des actions personnalisées en dérivant de ClaimAction et en implémentant la méthode abstraite Run.

Pour plus d’informations, consultez Microsoft.AspNetCore.Authentication.OAuth.Claims.

Ajouter et mettre à jour des revendications utilisateur

Les revendications sont copiées à partir de fournisseurs externes dans la base de données utilisateur lors de la première inscription, et non lors de la connexion. Si des revendications supplémentaires sont activées dans une application après qu’un utilisateur s’est inscrit pour utiliser l’application, appelez SignInManager.RefreshSignInAsync sur un utilisateur pour forcer la génération d’une nouvelle authentification cookie.

Dans l’environnement de développement qui fonctionne avec des comptes d’utilisateur de test, supprimez et recréez le compte d’utilisateur. Pour les systèmes de production, les nouvelles revendications ajoutées à l’application peuvent être renvoyées dans des comptes d’utilisateur. Après avoir généré la structure de la pageExternalLogin dans l’application sur Areas/Pages/Identity/Account/Manage, ajoutez le code suivant à ExternalLoginModel dans le fichier ExternalLogin.cshtml.cs.

Ajoute un dictionnaire des revendications ajoutées. Utilisez les clés de dictionnaire pour contenir les types de revendication et utilisez les valeurs pour contenir une valeur par défaut. Ajoutez le code suivant en haut de la classe. L’exemple suivant suppose qu’une revendication est ajoutée pour l’image Google de l’utilisateur avec une image en tête générique comme valeur par défaut :

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

Remplacez la méthode OnGetCallbackAsync par défaut par le code suivant. Le code effectue des boucles dans le dictionnaire de revendications. Les revendications sont ajoutées (renvoyées) ou mises à jour pour chaque utilisateur. Lorsque des revendications sont ajoutées ou mises à jour, la connexion utilisateur est actualisée à l’aide de SignInManager<TUser>, en conservant les propriétés d’authentification existantes (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();
    }
}

Une approche similaire est adoptée lorsque les revendications changent lorsqu’un utilisateur est connecté, mais qu’une étape de renvoi n’est pas requise. Pour mettre à jour les revendications d’un utilisateur, appelez ce qui suit sur l’utilisateur :

Supprimer les actions de revendication et les revendications

ClaimActionCollection.Remove(String) supprime toutes les actions de revendication pour le ClaimType de la collection. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) supprime une revendication du ClaimType de l’identité. DeleteClaim est principalement utilisé avec OpenID Connect (OIDC) pour supprimer les revendications générées par le protocole.

Exemple de sortie d’application

Exécutez l’exemple d’application et sélectionnez le lien 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

Transférer les informations sur la demande avec un proxy ou un équilibreur de charge

Si l’application est déployée derrière un serveur proxy ou un équilibreur de charge, certaines informations sur la demande d’origine peuvent être transférées vers l’application dans les en-têtes de demande. Ces informations incluent généralement le schéma de demande sécurisé (https), l’hôte et l’adresse IP du client. Les applications ne lisent pas automatiquement ces en-têtes de demande pour découvrir et d’utiliser les informations sur la demande d’origine.

Le schéma est utilisé dans la génération de lien qui affecte le flux d’authentification dans le cas de fournisseurs externes. En cas de perte du schéma sécurisé (https), l’application génère des URL de redirection incorrectes et non sécurisées.

Utilisez l’intergiciel Forwarded Headers afin de mettre les informations de demande d’origine à la disposition de l’application pour le traitement des demandes.

Pour plus d’informations, consultez l’article Configurer ASP.NET Core pour l’utilisation de serveurs proxy et d’équilibreurs de charge.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Une application ASP.NET Core peut établir des revendications et des jetons supplémentaires à partir de fournisseurs d’authentification externes, tels que Facebook, Google, Microsoft et Twitter. Chaque fournisseur révèle des informations différentes concernant les utilisateurs sur sa plateforme, mais le modèle de réception et de transformation des données utilisateur en revendications supplémentaires reste le même.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Prérequis

Déterminez les fournisseurs d’authentification externes à prendre en charge dans l’application. Pour chaque fournisseur, inscrivez l’application et obtenez un ID client et une clé secrète client. Pour plus d’informations, consultez Authentification Facebook et Google dans ASP.NET Core. L’exemple d’application utilise le fournisseur d’authentification Google.

Définir l’ID client et le secret client

Le fournisseur d’authentification OAuth établit une relation d’approbation avec une application à l’aide d’un ID client et d’une clé secrète client. Les valeurs d’ID client et de clé secrète client sont créées pour l’application par le fournisseur d’authentification externe lorsque l’application est inscrite auprès du fournisseur. Chaque fournisseur externe utilisé par l’application doit être configuré indépendamment avec l’ID client et la clé secrète client du fournisseur. Pour plus d’informations, consultez les rubriques sur les fournisseurs d’authentification externes qui s’appliquent à votre scénario :

Les revendications facultatives envoyées dans l’ID ou le jeton d’accès du fournisseur d’authentification sont généralement configurées dans le portail en ligne du fournisseur. Par exemple, Microsoft Azure Active Directory (AAD) permet d’attribuer des revendications facultatives au jeton d’ID de l’application dans le panneau Configuration du jeton de l’inscription de l’application. Pour plus d’informations, consultez Guide pratique pour fournir des revendications facultatives à votre application (documentation Azure). Pour les autres fournisseurs, consultez leurs ensembles de documentation externes.

L’exemple d’application configure le fournisseur d’authentification Google avec un ID client et une clé secrète client fournis par 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;
    };
});

Établir l’étendue de l’authentification

Définissez la liste des autorisations à récupérer à partir du fournisseur en spécifiant Scope. Les étendues d’authentification pour les fournisseurs externes courants apparaissent dans le tableau suivant.

Fournisseur Portée
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

Dans l’exemple d’application, les étendues profile, email et openid de Google sont automatiquement ajoutées par l’infrastructure quand AddGoogle est appelée sur AuthenticationBuilder. Si l’application nécessite des étendues supplémentaires, ajoutez-les aux options. Dans l’exemple suivant, l’étendue Google https://www.googleapis.com/auth/user.birthday.read est ajoutée pour récupérer l’anniversaire d’un utilisateur :

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

Mapper les clés de données utilisateur et créer des revendications

Dans les options du fournisseur, spécifiez un MapJsonKey ou MapJsonSubKey pour chaque clé ou sous-clé dans le JS du fournisseur externe SUR les données utilisateur pour que l’identité de l’application soit lue lors de la connexion. Pour plus d’informations sur les nouveaux types de revendications, consultez ClaimTypes.

L’exemple d’application crée des revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) à partir des clés locale et picture dans les données utilisateur 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;
    };
});

Dans Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, un IdentityUser (ApplicationUser) est connecté à l’application avec SignInAsync. Pendant le processus de connexion, UserManager<TUser> peut stocker des revendications ApplicationUser pour les données utilisateur disponibles à partir de Principal.

Dans l’exemple d’application, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) établit les revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) pour l’ApplicationUser connecté, y compris une revendication pour 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();
}

Par défaut, les revendications d’un utilisateur sont stockées dans l’authentification cookie. Si l’authentification cookie est trop grande, cela peut entraîner l’échec de l’application pour les raisons suivantes :

  • Le navigateur détecte que l’en-tête cookie est trop long.
  • La taille globale de la requête est trop grande.

Si une grande quantité de données utilisateur est requise pour le traitement des demandes des utilisateurs :

  • Limitez le nombre et la taille des revendications utilisateur pour le traitement des demandes uniquement à ce dont l’application a besoin.
  • Utilisez un logiciel personnalisé ITicketStore pour le Cookie de l’intergiciel d’authentification pour stocker l’identité SessionStore entre les requêtes. Conservez de grandes quantités d’informations d’identité sur le serveur tout en envoyant une petite clé d’identificateur de session au client.

Enregistrer le jeton d’accès

SaveTokens définit si les jetons d’accès et d’actualisation doivent être stockés dans AuthenticationProperties après une autorisation réussie. SaveTokens est défini sur false par défaut pour réduire la taille de l’authentification finale cookie.

L’exemple d’application définit la valeur de SaveTokens sur true dans 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;
    };
});

Lors de l’exécution de OnPostConfirmationAsync, stockez le jeton d’accès (ExternalLoginInfo.AuthenticationTokens) à partir du fournisseur externe dans le AuthenticationProperties de ApplicationUser.

L’exemple d’application enregistre le jeton d’accès dans OnPostConfirmationAsync (nouvelle inscription de l’utilisateur) et OnGetCallbackAsync (utilisateur précédemment inscrit) dans 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();
}

Remarque

Pour plus d’informations sur la transmission de jetons aux composants Razor d’une application Blazor côté serveur, consultez les scénarios de sécurité supplémentaire de ASP.NET Core côté serveurBlazor.

Comment ajouter des jetons personnalisés supplémentaires

Pour montrer comment ajouter un jeton personnalisé, qui est stocké dans le cadre de SaveTokens, l’exemple d’application ajoute un AuthenticationToken avec le DateTime actuel pour 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;
    };
});

Création et ajout de revendications

L’infrastructure fournit des actions courantes et des méthodes d’extension pour créer et ajouter des revendications à la collection. Pour plus d’informations, consultez ClaimActionCollectionMapExtensions et ClaimActionCollectionUniqueExtensions.

Les utilisateurs peuvent définir des actions personnalisées en dérivant de ClaimAction et en implémentant la méthode abstraite Run.

Pour plus d’informations, consultez Microsoft.AspNetCore.Authentication.OAuth.Claims.

Ajouter et mettre à jour des revendications utilisateur

Les revendications sont copiées à partir de fournisseurs externes dans la base de données utilisateur lors de la première inscription, et non lors de la connexion. Si des revendications supplémentaires sont activées dans une application après qu’un utilisateur s’est inscrit pour utiliser l’application, appelez SignInManager.RefreshSignInAsync sur un utilisateur pour forcer la génération d’une nouvelle authentification cookie.

Dans l’environnement de développement qui fonctionne avec des comptes d’utilisateur de test, il vous suffit de supprimer et recréer le compte d’utilisateur. Pour les systèmes de production, les nouvelles revendications ajoutées à l’application peuvent être renvoyées dans des comptes d’utilisateur. Après avoir généré la structure de la pageExternalLogin dans l’application sur Areas/Pages/Identity/Account/Manage, ajoutez le code suivant à ExternalLoginModel dans le fichier ExternalLogin.cshtml.cs.

Ajoute un dictionnaire des revendications ajoutées. Utilisez les clés de dictionnaire pour contenir les types de revendication et utilisez les valeurs pour contenir une valeur par défaut. Ajoutez le code suivant en haut de la classe. L’exemple suivant suppose qu’une revendication est ajoutée pour l’image Google de l’utilisateur avec une image en tête générique comme valeur par défaut :

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

Remplacez la méthode OnGetCallbackAsync par défaut par le code suivant. Le code effectue des boucles dans le dictionnaire de revendications. Les revendications sont ajoutées (renvoyées) ou mises à jour pour chaque utilisateur. Lorsque des revendications sont ajoutées ou mises à jour, la connexion utilisateur est actualisée à l’aide de SignInManager<TUser>, en conservant les propriétés d’authentification existantes (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();
    }
}

Une approche similaire est adoptée lorsque les revendications changent lorsqu’un utilisateur est connecté, mais qu’une étape de renvoi n’est pas requise. Pour mettre à jour les revendications d’un utilisateur, appelez ce qui suit sur l’utilisateur :

Suppression des actions de revendication et des revendications

ClaimActionCollection.Remove(String) supprime toutes les actions de revendication pour le ClaimType de la collection. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) supprime une revendication du ClaimType de l’identité. DeleteClaim est principalement utilisé avec OpenID Connect (OIDC) pour supprimer les revendications générées par le protocole.

Exemple de sortie d’application

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

Transférer les informations sur la demande avec un proxy ou un équilibreur de charge

Si l’application est déployée derrière un serveur proxy ou un équilibreur de charge, certaines informations sur la demande d’origine peuvent être transférées vers l’application dans les en-têtes de demande. Ces informations incluent généralement le schéma de demande sécurisé (https), l’hôte et l’adresse IP du client. Les applications ne lisent pas automatiquement ces en-têtes de demande pour découvrir et d’utiliser les informations sur la demande d’origine.

Le schéma est utilisé dans la génération de lien qui affecte le flux d’authentification dans le cas de fournisseurs externes. En cas de perte du schéma sécurisé (https), l’application génère des URL de redirection incorrectes et non sécurisées.

Utilisez l’intergiciel Forwarded Headers afin de mettre les informations de demande d’origine à la disposition de l’application pour le traitement des demandes.

Pour plus d’informations, consultez l’article Configurer ASP.NET Core pour l’utilisation de serveurs proxy et d’équilibreurs de charge.

Une application ASP.NET Core peut établir des revendications et des jetons supplémentaires à partir de fournisseurs d’authentification externes, tels que Facebook, Google, Microsoft et Twitter. Chaque fournisseur révèle des informations différentes concernant les utilisateurs sur sa plateforme, mais le modèle de réception et de transformation des données utilisateur en revendications supplémentaires reste le même.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Prérequis

Déterminez les fournisseurs d’authentification externes à prendre en charge dans l’application. Pour chaque fournisseur, inscrivez l’application et obtenez un ID client et une clé secrète client. Pour plus d’informations, consultez Authentification Facebook et Google dans ASP.NET Core. L’exemple d’application utilise le fournisseur d’authentification Google.

Définir l’ID client et le secret client

Le fournisseur d’authentification OAuth établit une relation d’approbation avec une application à l’aide d’un ID client et d’une clé secrète client. Les valeurs d’ID client et de clé secrète client sont créées pour l’application par le fournisseur d’authentification externe lorsque l’application est inscrite auprès du fournisseur. Chaque fournisseur externe utilisé par l’application doit être configuré indépendamment avec l’ID client et la clé secrète client du fournisseur. Pour plus d’informations, consultez les rubriques sur les fournisseurs d’authentification externes qui s’appliquent à votre scénario :

L’exemple d’application configure le fournisseur d’authentification Google avec un ID client et une clé secrète client fournis par 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;
    };
});

Établir l’étendue de l’authentification

Définissez la liste des autorisations à récupérer à partir du fournisseur en spécifiant Scope. Les étendues d’authentification pour les fournisseurs externes courants apparaissent dans le tableau suivant.

Fournisseur Portée
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

Dans l’exemple d’application, l’étendue userinfo.profile de Google est automatiquement ajoutée par l’infrastructure quand AddGoogle est appelée sur AuthenticationBuilder. Si l’application nécessite des étendues supplémentaires, ajoutez-les aux options. Dans l’exemple suivant, l’étendue Google https://www.googleapis.com/auth/user.birthday.read est ajoutée pour récupérer l’anniversaire d’un utilisateur :

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

Mapper les clés de données utilisateur et créer des revendications

Dans les options du fournisseur, spécifiez un MapJsonKey ou MapJsonSubKey pour chaque clé ou sous-clé dans le JS du fournisseur externe SUR les données utilisateur pour que l’identité de l’application soit lue lors de la connexion. Pour plus d’informations sur les nouveaux types de revendications, consultez ClaimTypes.

L’exemple d’application crée des revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) à partir des clés locale et picture dans les données utilisateur 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;
    };
});

Dans Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, un IdentityUser (ApplicationUser) est connecté à l’application avec SignInAsync. Pendant le processus de connexion, UserManager<TUser> peut stocker des revendications ApplicationUser pour les données utilisateur disponibles à partir de Principal.

Dans l’exemple d’application, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) établit les revendications de paramètres régionaux (urn:google:locale) et d’image (urn:google:picture) pour l’ApplicationUser connecté, y compris une revendication pour 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();
}

Par défaut, les revendications d’un utilisateur sont stockées dans l’authentification cookie. Si l’authentification cookie est trop grande, cela peut entraîner l’échec de l’application pour les raisons suivantes :

  • Le navigateur détecte que l’en-tête cookie est trop long.
  • La taille globale de la requête est trop grande.

Si une grande quantité de données utilisateur est requise pour le traitement des demandes des utilisateurs :

  • Limitez le nombre et la taille des revendications utilisateur pour le traitement des demandes uniquement à ce dont l’application a besoin.
  • Utilisez un logiciel personnalisé ITicketStore pour le Cookie de l’intergiciel d’authentification pour stocker l’identité SessionStore entre les requêtes. Conservez de grandes quantités d’informations d’identité sur le serveur tout en envoyant une petite clé d’identificateur de session au client.

Enregistrer le jeton d’accès

SaveTokens définit si les jetons d’accès et d’actualisation doivent être stockés dans AuthenticationProperties après une autorisation réussie. SaveTokens est défini sur false par défaut pour réduire la taille de l’authentification finale cookie.

L’exemple d’application définit la valeur de SaveTokens sur true dans 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;
    };
});

Lors de l’exécution de OnPostConfirmationAsync, stockez le jeton d’accès (ExternalLoginInfo.AuthenticationTokens) à partir du fournisseur externe dans le AuthenticationProperties de ApplicationUser.

L’exemple d’application enregistre le jeton d’accès dans OnPostConfirmationAsync (nouvelle inscription de l’utilisateur) et OnGetCallbackAsync (utilisateur précédemment inscrit) dans 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();
}

Comment ajouter des jetons personnalisés supplémentaires

Pour montrer comment ajouter un jeton personnalisé, qui est stocké dans le cadre de SaveTokens, l’exemple d’application ajoute un AuthenticationToken avec le DateTime actuel pour 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;
    };
});

Création et ajout de revendications

L’infrastructure fournit des actions courantes et des méthodes d’extension pour créer et ajouter des revendications à la collection. Pour plus d’informations, consultez ClaimActionCollectionMapExtensions et ClaimActionCollectionUniqueExtensions.

Les utilisateurs peuvent définir des actions personnalisées en dérivant de ClaimAction et en implémentant la méthode abstraite Run.

Pour plus d’informations, consultez Microsoft.AspNetCore.Authentication.OAuth.Claims.

Suppression des actions de revendication et des revendications

ClaimActionCollection.Remove(String) supprime toutes les actions de revendication pour le ClaimType de la collection. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) supprime une revendication du ClaimType de l’identité. DeleteClaim est principalement utilisé avec OpenID Connect (OIDC) pour supprimer les revendications générées par le protocole.

Exemple de sortie d’application

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

Transférer les informations sur la demande avec un proxy ou un équilibreur de charge

Si l’application est déployée derrière un serveur proxy ou un équilibreur de charge, certaines informations sur la demande d’origine peuvent être transférées vers l’application dans les en-têtes de demande. Ces informations incluent généralement le schéma de demande sécurisé (https), l’hôte et l’adresse IP du client. Les applications ne lisent pas automatiquement ces en-têtes de demande pour découvrir et d’utiliser les informations sur la demande d’origine.

Le schéma est utilisé dans la génération de lien qui affecte le flux d’authentification dans le cas de fournisseurs externes. En cas de perte du schéma sécurisé (https), l’application génère des URL de redirection incorrectes et non sécurisées.

Utilisez l’intergiciel Forwarded Headers afin de mettre les informations de demande d’origine à la disposition de l’application pour le traitement des demandes.

Pour plus d’informations, consultez l’article Configurer ASP.NET Core pour l’utilisation de serveurs proxy et d’équilibreurs de charge.

Ressources supplémentaires

  • Application Dotnet/AspNetCore engineering SocialSample : l’exemple d’application lié se trouve sur la branche d’ingénierie du dépôt GitHub dotnet/AspNetCore.main La branche main contient du code en cours de développement actif pour la prochaine version de ASP.NET Core. Pour afficher une version de l’exemple d’application pour une version publiée de ASP.NET Core, utilisez la liste déroulante Branche pour sélectionner une branche de mise en production (par exemplerelease/{X.Y}).