web-dev-qa-db-fra.com

API Web .Net Core 2.0 utilisant JWT - L'ajout d'une identité rompt l'authentification JWT

(Éditer - Correction appropriée trouvée! Voir ci-dessous)

OK - c'est ma première tentative de .Net Core 2.0 et d'authentification, bien que j'aie fait des choses avec Web API 2.0 dans le passé et que j'ai travaillé assez largement sur divers MVC et Webforms ASP projets au cours des deux dernières années.

J'essaie de créer un projet d'API Web UNIQUEMENT à l'aide de .Net Core. Cela formera le back-end d'une application multi-locataire pour la production de certains rapports, je dois donc être en mesure d'authentifier les utilisateurs. Il semble que l'approche habituelle consiste à utiliser JWT - authentifiez d'abord l'utilisateur pour générer un jeton, puis transmettez-le au client pour l'utiliser à chaque demande d'API. Les données seront stockées et récupérées à l'aide d'EF Core.

J'ai suivi cet article pour un moyen de base pour configurer cela, et j'ai réussi à le faire fonctionner correctement - j'ai un contrôleur qui accepte un nom d'utilisateur/mot de passe et renvoie un jeton s'il est valide, et certains Politiques d'autorisation établies en fonction des réclamations.

La prochaine chose dont j'ai besoin est de gérer réellement les utilisateurs/mots de passe/etc. Je pensais que j'utiliserais juste .Net Core Identity pour cela car de cette façon, j'aurais beaucoup de code prêt à l'emploi pour vous soucier des utilisateurs/rôles, des mots de passe, etc. J'utilisais une classe User personnalisée et UserRole classes dérivées des classes standard IdentityUser et IdentityRole, mais depuis, je suis revenu aux classes standard.

Le problème que j'ai, c'est que je n'arrive pas à comprendre comment ajouter une identité et enregistrer tous les différents services (rolemanager, usermanager, etc.) sans interrompre également l'authentification - essentiellement dès que j'ajoute cette ligne à mon Startup.ConfigureServices classe:

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<MyContext>();

Tout va mal, et je ne vois plus aucune réclamation lorsque je reçois une demande, donc toutes les politiques se verrouillent et vous ne pouvez rien faire.

Si je n'ai pas ces lignes, je me retrouve avec des erreurs liées à UserManager, RoleManager, UserStore, etc. qui ne sont pas toutes enregistrées pour DI.

Alors ... comment (si c'est possible) puis-je enregistrer l'identité et la connecter correctement au contexte, mais éviter/supprimer toute modification du mécanisme d'autorisation réel?

J'ai regardé un peu en ligne, mais beaucoup de choses ont changé depuis .Net Core 1.x, donc beaucoup de tutoriels, etc. ne sont plus vraiment valides.

Je n'ai pas l'intention que cette application API ait un code frontal, donc je n'ai pas besoin d'authentification par cookie pour les formulaires ou quoi que ce soit pour l'instant.

Modifier
ok, j'ai maintenant trouvé que dans ce code, configurer l'authentification JWT dans la méthode Startup.ConfigureServices():

 services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                 >>breakpoint>>>   options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateLifetime = true,
                            ValidateIssuerSigningKey = true,

                            ValidIssuer = "Blah.Blah.Bearer",
                            ValidAudience = "Blah.Blah.Bearer",
                            IssuerSigningKey =
                            JwtSecurityKey.Create("verylongsecretkey")

                        };
                });

Si je mets un point d'arrêt à la ligne indiquée (via ">> breakpoint >>>") alors il est touché lorsque je ne le faites pas ajoute les lignes pour ajouter des services d'identité, mais si je le fais ajoutez ces lignes puis il jamais est touché. Cela est vrai, peu importe où dans la méthode je mets l'appel services.AddIdentity(). J'obtiens que c'est simplement un lambda donc il est exécuté plus tard, mais est-il possible d'obtenir le truc AddIdentity pour ne PAS configurer l'authentification, ou faire supprimer immédiatement le code? Je suppose qu'à un moment donné, il y a du code qui choisit de ne pas exécuter Lambda pour la configuration que j'ai définie là-bas, car les éléments d'identité l'ont déjà défini ...

Merci d'avoir lu tout ça si vous en avez :)

EDIT - Trouvé une réponse
ok, j'ai finalement trouvé ce problème de GH qui est essentiellement exactement ce problème: https://github.com/aspnet/Identity/issues/1376

Fondamentalement, ce que je devais faire était double:

Assurez-vous que l'appel à services.AddIdentity<IdentityUser, IdentityContext() a été effectué d'abord

Modifiez l'appel pour ajouter l'authentification à partir de:

services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
...

À:

services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(options =>
...

Cela entraîne de manière ennuyeuse la création d'un cookie, mais cela n'est pas utilisé pour l'authentification pour autant que je sache - il utilise uniquement le jeton du porteur sur les demandes aux contrôleurs/actions qui ont [Authorize(Policy = "Administrator")] ou un ensemble similaire au moins.

J'ai besoin de tester plus, et je vais essayer de revenir ici une mise à jour si je trouve que cela ne fonctionne pas d'une manière ou d'une autre.

(Modifié - mettez la solution appropriée comme réponse maintenant)

18
GPW

J'ai finalement mis au point la solution, donc sur la suggestion de l'utilisateur de toujours apprendre, j'ai édité mon message et je mets cela en tant que réponse réelle.

ok, cela peut être fait correctement. Tout d'abord, vous devez utiliser les options d'authentification que j'ai soulignées dans ma modification ci-dessus - c'est bien. Ensuite, vous devez utiliserservices.AddIdentityCore<TUser>() plutôt que services.AddIdentity<TUser>(). Cependant, cela n'ajoute pas tout un tas de choses pour la gestion des rôles et manque apparemment du constructeur approprié pour lui donner le type de rôle que vous souhaitez utiliser. Cela signifie que dans mon cas, je devais faire ceci:

  IdentityBuilder builder = services.AddIdentityCore<IdentityUser>(opt =>
        {
            opt.Password.RequireDigit = true;
            opt.Password.RequiredLength = 8;
            opt.Password.RequireNonAlphanumeric = false;
            opt.Password.RequireUppercase = true;
            opt.Password.RequireLowercase = true;
        }
        );
        builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
        builder
            .AddEntityFrameworkStores<MyContext>();
        //.AddDefaultTokenProviders();

        builder.AddRoleValidator<RoleValidator<IdentityRole>>();
        builder.AddRoleManager<RoleManager<IdentityRole>>();
        builder.AddSignInManager<SignInManager<IdentityUser>>();

Cela fait, la prochaine chose est de vous assurer que lors de la validation d'une connexion utilisateur (avant d'envoyer un jeton), vous devez utiliser la méthode SignInManager CheckPasswordSignInAsync et notPasswordSignInAsync:

public async Task<IdentityUser> GetUserForLogin(string userName, string password)
    {   
        //find user first...
        var user = await _userManager.FindByNameAsync(userName);

        if (user == null)
        {
            return null;
        }

        //validate password...
        var signInResult = await _signInManager.CheckPasswordSignInAsync(user, password, false);

        //if password was ok, return this user.
        if (signInResult.Succeeded)
        {
            return user;
        }

        return null;
    }

si vous utilisez la méthode PasswordSignInAsync, vous obtiendrez alors une erreur d'exécution. Aucun IAuthenticationSignInHandler en cours de configuration.

J'espère que cela aide quelqu'un à un moment donné.

23
GPW

J'ai extrait le code AddIdentity de github et créé une méthode d'extension basée sur celui-ci qui n'ajoute pas l'authentificateur de cookie par défaut, il est maintenant assez similaire au AddIdentityCore intégré mais peut accepter IdentityRole.

/// <summary>
/// Contains extension methods to <see cref="IServiceCollection"/> for configuring identity services.
/// </summary>
public static class IdentityServiceExtensions
{
    /// <summary>
    /// Adds the default identity system configuration for the specified User and Role types. (Without Authentication Scheme)
    /// </summary>
    /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
    /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
    /// <param name="services">The services available in the application.</param>
    /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
    public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services)
        where TUser : class
        where TRole : class
        => services.AddIdentityWithoutAuthenticator<TUser, TRole>(setupAction: null);

    /// <summary>
    /// Adds and configures the identity system for the specified User and Role types. (Without Authentication Scheme)
    /// </summary>
    /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
    /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
    /// <param name="services">The services available in the application.</param>
    /// <param name="setupAction">An action to configure the <see cref="IdentityOptions"/>.</param>
    /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
    public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
        where TUser : class
        where TRole : class
    {
        // Hosting doesn't add IHttpContextAccessor by default
        services.AddHttpContextAccessor();
        // Identity services
        services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
        services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
        services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
        services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
        services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
        // No interface for the error describer so we can add errors without rev'ing the interface
        services.TryAddScoped<IdentityErrorDescriber>();
        services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
        services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
        services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
        services.TryAddScoped<UserManager<TUser>>();
        services.TryAddScoped<SignInManager<TUser>>();
        services.TryAddScoped<RoleManager<TRole>>();

        if (setupAction != null)
        {
            services.Configure(setupAction);
        }

        return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
    }
}

Vous pouvez maintenant utiliser le code ci-dessus normalement à partir d'un projet WebApi comme

.AddIdentityWithoutAuthenticator<User, IdentityRole>()
2
Ricky Spanish