web-dev-qa-db-fra.com

Authentification basée sur les jetons dans ASP.NET Core

Je travaille avec l'application ASP.NET Core. J'essaie d'implémenter l'authentification basée sur les jetons, mais je ne peux pas comprendre comment utiliser new Security System pour mon cas. J'ai traversé exemples mais ils ne m'ont pas beaucoup aidé, ils utilisent soit l'authentification par cookie, soit l'authentification externe (GitHub, Microsoft, Twitter).

Quel est mon scénario? L'application angularjs devrait demander /token url en passant le nom d'utilisateur et le mot de passe. WebApi doit autoriser l'utilisateur et renvoyer access_token qui sera utilisé par l'application angularjs dans les requêtes suivantes.

J'ai trouvé un excellent article sur la mise en œuvre exacte de ce dont j'ai besoin dans la version actuelle d'ASP.NET - Authentification basée sur les jetons à l'aide de l'API Web ASP.NET 2, Owin et Identity . Mais il n’est pas évident pour moi de faire la même chose dans ASP.NET Core.

Ma question est la suivante: comment configurer l'application ASP.NET Core WebApi pour fonctionner avec l'authentification par jeton?

153
Grant

Mise à jour pour .Net Core 2:

Les versions précédentes de cette réponse utilisaient RSA; ce n'est vraiment pas nécessaire si votre code qui génère les jetons vérifie également les jetons. Toutefois, si vous distribuez la responsabilité, vous souhaiterez probablement toujours utiliser une instance de _Microsoft.IdentityModel.Tokens.RsaSecurityKey_.

  1. Créez quelques constantes que nous utiliserons plus tard. voici ce que j'ai fait:

    _const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    _
  2. Ajoutez ceci à votre ConfigureServices de votre Startup.cs. Nous utiliserons l'injection de dépendance ultérieurement pour accéder à ces paramètres. Je suppose que votre authenticationConfiguration est un objet ConfigurationSection ou Configuration tel que vous pouvez avoir une configuration différente pour le débogage et la production. Assurez-vous de stocker votre clé en toute sécurité! Cela peut être n'importe quelle chaîne.

    _var keySecret = authenticationConfiguration["JwtSigningKey"];
    var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret));
    
    services.AddTransient(_ => new JwtSignInHandler(symmetricKey));
    
    services.AddAuthentication(options =>
    {
        // This causes the default authentication scheme to be JWT.
        // Without this, the Authorization header is not checked and
        // you'll get no results. However, this also means that if
        // you're already using cookies in your app, they won't be 
        // checked by default.
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters.ValidateIssuerSigningKey = true;
            options.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });
    _

    J'ai vu d'autres réponses modifier d'autres paramètres, tels que ClockSkew; les valeurs par défaut sont définies de telle sorte que cela devrait fonctionner pour les environnements distribués dont les horloges ne sont pas parfaitement synchronisées. Ce sont les seuls paramètres que vous devez modifier.

  3. Configurer l'authentification. Vous devriez avoir cette ligne avant tout middleware nécessitant votre information User, telle que app.UseMvc().

    _app.UseAuthentication();
    _

    Notez que cela n'entraînera pas l'émission de votre jeton avec le SignInManager ou quoi que ce soit d'autre. Vous devrez fournir votre propre mécanisme de sortie de votre JWT - voir ci-dessous.

  4. Vous voudrez peut-être spécifier un AuthorizationPolicy. Cela vous permettra de spécifier des contrôleurs et des actions n'autorisant les jetons de support que comme authentification à l'aide de [Authorize("Bearer")].

    _services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
    _
  5. Voici la partie la plus délicate: la construction du jeton.

    _class JwtSignInHandler
    {
        public const string TokenAudience = "Myself";
        public const string TokenIssuer = "MyProject";
        private readonly SymmetricSecurityKey key;
    
        public JwtSignInHandler(SymmetricSecurityKey symmetricKey)
        {
            this.key = symmetricKey;
        }
    
        public string BuildJwt(ClaimsPrincipal principal)
        {
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
            var token = new JwtSecurityToken(
                issuer: TokenIssuer,
                audience: TokenAudience,
                claims: principal.Claims,
                expires: DateTime.Now.AddMinutes(20),
                signingCredentials: creds
            );
    
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
    _

    Ensuite, dans votre contrôleur où vous voulez que votre jeton, quelque chose comme ce qui suit:

    _[HttpPost]
    public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory)
    {
        var principal = new System.Security.Claims.ClaimsPrincipal(new[]
        {
            new System.Security.Claims.ClaimsIdentity(new[]
            {
                new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User")
            })
        });
        return tokenFactory.BuildJwt(principal);
    }
    _

    Ici, je suppose que vous avez déjà un principal. Si vous utilisez Identity, vous pouvez utiliser IUserClaimsPrincipalFactory<> pour transformer votre User en un ClaimsPrincipal.

  6. Pour le tester : Obtenez un jeton, mettez-le dans le formulaire sous jwt.io . Les instructions que j'ai fournies ci-dessus vous permettent également d'utiliser le secret de votre configuration pour valider la signature!

  7. Si vous présentiez cela sous forme de vue partielle sur votre page HTML en combinaison avec l'authentification par support dans Net 4.5, vous pouvez désormais utiliser un ViewComponent pour faire de même. C'est en grande partie le même que le code Action du contrôleur ci-dessus.

127
Matt DeKrey

À partir de réponse fabuleuse de Matt Dekrey , j'ai créé un exemple pleinement fonctionnel d'authentification basée sur des jetons, qui fonctionne avec ASP.NET Core (1.0.1). Vous pouvez trouver le code complet dans ce dépôt sur GitHub (branches alternatives pour 1.0.0-rc1 , beta8 , beta7 ), mais en résumé, les étapes importantes sont les suivantes:

Générez une clé pour votre application

Dans mon exemple, je génère une clé aléatoire à chaque démarrage de l'application. Vous devez en générer une, la stocker quelque part et la fournir à votre application. Voir ce fichier pour savoir comment je génère une clé aléatoire et comment l’importer à partir d’un fichier .json . Comme suggéré dans les commentaires de @kspearrin, le API de protection des données semble être le candidat idéal pour gérer les clés "correctement", mais je n'ai pas encore déterminé si cela était possible. S'il vous plaît soumettre une demande de traction si vous vous en sortez!

Startup.cs - ConfigureServices

Ici, nous devons charger une clé privée pour la signature de nos jetons, que nous utiliserons également pour vérifier les jetons tels qu'ils sont présentés. Nous stockons la clé dans une variable de classe key que nous réutiliserons dans la méthode Configure ci-dessous. TokenAuthOptions est une classe simple contenant l'identité de la signature, l'audience et l'émetteur dont nous avons besoin dans TokenController pour créer nos clés.

// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

// Create the key, and a set of token options to record signing credentials 
// using that key, along with the other parameters we will need in the 
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
    Audience = TokenAudience,
    Issuer = TokenIssuer,
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};

// Save the token options into an instance so they're accessible to the 
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);

// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
        .RequireAuthenticatedUser().Build());
});

Nous avons également défini une politique d'autorisation nous permettant d'utiliser [Authorize("Bearer")] sur les ordinateurs d'extrémité et les classes que nous souhaitons protéger.

Startup.cs - Configurez

Ici, nous devons configurer l’authentification JwtBearerAuthentication:

app.UseJwtBearerAuthentication(new JwtBearerOptions {
    TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = key,
        ValidAudience = tokenOptions.Audience,
        ValidIssuer = tokenOptions.Issuer,

        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,

        // This defines the maximum allowable clock skew - i.e.
        // provides a tolerance on the token expiry time 
        // when validating the lifetime. As we're creating the tokens 
        // locally and validating them on the same machines which 
        // should have synchronised time, this can be set to zero. 
        // Where external tokens are used, some leeway here could be 
        // useful.
        ClockSkew = TimeSpan.FromMinutes(0)
    }
});

TokenController

Dans le contrôleur de jeton, vous devez disposer d'une méthode pour générer des clés signées à l'aide de la clé chargée dans Startup.cs. Nous avons enregistré une instance TokenAuthOptions dans Startup, nous devons donc l'injecter dans le constructeur de TokenController:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

Ensuite, vous devrez générer le jeton dans votre gestionnaire pour le noeud final de connexion. Dans mon exemple, je prends un nom d'utilisateur et un mot de passe et les valide à l'aide d'une instruction if, mais l'essentiel est de créer ou de charger des revendications. identité basée sur le client et générer le jeton correspondant:

public class AuthRequest
{
    public string username { get; set; }
    public string password { get; set; }
}

/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
    // Obviously, at this point you need to validate the username and password against whatever system you wish.
    if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
    {
        DateTime? expires = DateTime.UtcNow.AddMinutes(2);
        var token = GetToken(req.username, expires);
        return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
    }
    return new { authenticated = false };
}

private string GetToken(string user, DateTime? expires)
{
    var handler = new JwtSecurityTokenHandler();

    // Here, you should create or look up an identity for the user which is being authenticated.
    // For now, just creating a simple generic identity.
    ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });

    var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
        Issuer = tokenOptions.Issuer,
        Audience = tokenOptions.Audience,
        SigningCredentials = tokenOptions.SigningCredentials,
        Subject = identity,
        Expires = expires
    });
    return handler.WriteToken(securityToken);
}

Et ça devrait être ça. Ajoutez simplement [Authorize("Bearer")] à toute méthode ou classe que vous souhaitez protéger, et vous devriez obtenir une erreur si vous essayez d'y accéder sans la présence d'un jeton. Si vous voulez renvoyer une erreur 401 au lieu d'une erreur 500, vous devrez enregistrer un gestionnaire d'exceptions personnalisé comme je l'ai dans mon exemple ici .

80
Mark Hughes

Vous pouvez consulter les exemples de connexion OpenId qui illustrent comment traiter différents mécanismes d'authentification, notamment les jetons JWT:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

Si vous examinez le projet Cordova Backend, la configuration de l'API est la suivante:

           // Create a new branch where the registered middleware will be executed only for non API calls.
        app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch => {
            // Insert a new cookies middleware in the pipeline to store
            // the user identity returned by the external identity provider.
            branch.UseCookieAuthentication(new CookieAuthenticationOptions {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                AuthenticationScheme = "ServerCookie",
                CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie",
                ExpireTimeSpan = TimeSpan.FromMinutes(5),
                LoginPath = new PathString("/signin"),
                LogoutPath = new PathString("/signout")
            });

            branch.UseGoogleAuthentication(new GoogleOptions {
                ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
                ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
            });

            branch.UseTwitterAuthentication(new TwitterOptions {
                ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
                ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
            });
        });

La logique de /Providers/AuthorizationProvider.cs et du RessourceController de ce projet mérite également d'être examinée;).

Sinon, vous pouvez également utiliser le code suivant pour valider les jetons (il y a aussi un extrait pour le faire fonctionner avec signalR):

        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;

            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

Pour émettre un jeton, vous pouvez utiliser les packages de serveur openId Connect comme suit:

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the authorization, logout, token and userinfo endpoints.
            //options.AuthorizationEndpointPath = "/connect/authorize";
            //options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            //options.UserinfoEndpointPath = "/connect/userinfo";

            // Note: if you don't explicitly register a signing key, one is automatically generated and
            // persisted on the disk. If the key cannot be persisted, an exception is thrown.
            // 
            // On production, using a X.509 certificate stored in the machine store is recommended.
            // You can generate a self-signed certificate using Pluralsight's self-cert utility:
            // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.Zip
            // 
            // options.SigningCredentials.AddCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
            // 
            // Alternatively, you can also store the certificate as an embedded .pfx resource
            // directly in this Assembly or in a file published alongside this project:
            // 
            // options.SigningCredentials.AddCertificate(
            //     Assembly: typeof(Startup).GetTypeInfo().Assembly,
            //     resource: "Nancy.Server.Certificate.pfx",
            //     password: "Owin.Security.OpenIdConnect.Server");

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            options.ApplicationCanDisplayErrors = true // in dev only ...;
            options.AllowInsecureHttp = true // in dev only...;
        });

EDIT: J'ai implémenté une application à page unique avec implémentation de l'authentification par jeton à l'aide de l'infrastructure frontale Aurelia et du noyau ASP.NET. Il existe également une connexion signal R persistante. Cependant, je n'ai pas implémenté de base de données. Le code peut être vu ici: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

J'espère que cela t'aides,

Meilleur,

Alex

3
Darxtar

Jetez un coup d’œil à OpenIddict - c’est un nouveau projet (au moment de la rédaction) qui facilite la configuration de la création des jetons JWT et la mise à jour des jetons dans ASP.NET 5. La validation des jetons est gérée par un autre logiciel.

En supposant que vous utilisiez Identity avec Entity Framework, la dernière ligne correspond à ce que vous ajouteriez à votre méthode ConfigureServices:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

Dans Configure, vous configurez OpenIddict pour servir les jetons JWT:

app.UseOpenIddictCore(builder =>
{
    // tell openiddict you're wanting to use jwt tokens
    builder.Options.UseJwtTokens();
    // NOTE: for dev consumption only! for live, this is not encouraged!
    builder.Options.AllowInsecureHttp = true;
    builder.Options.ApplicationCanDisplayErrors = true;
});

Vous configurez également la validation des jetons dans Configure:

// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

Il existe un ou deux autres éléments mineurs, tels que votre DbContext doit dériver de OpenIddictContext.

Vous pouvez voir une explication complète sur ce billet de blog: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Une démo fonctionnelle est disponible à l'adresse suivante: https://github.com/capesean/openiddict-test

1
Sean