web-dev-qa-db-fra.com

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

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 .

Mon scénario: Un client demande un jeton. Mon serveur doit autoriser l'utilisateur et renvoyer access_token qui sera utilisé par le client dans les requêtes suivantes.

Voici deux excellents articles sur la mise en œuvre de ce dont j'ai exactement besoin:

Le problème est qu'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 Web Api pour qu'elle fonctionne avec l'authentification par jeton? Quelle direction devrais-je poursuivre? Avez-vous écrit des articles sur la dernière version ou savez-vous où je pourrais en trouver?

Je vous remercie!

65

À 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 niveau 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 stratégie 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 .

70
Mark Hughes

C’est vraiment une copie de ne autre réponse de ma part , que j’ai tendance à tenir de plus en plus à jour au fur et à mesure de l’attention. Les commentaires peuvent aussi vous être utiles!

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. Cependant, si vous distribuez la responsabilité, vous souhaiterez probablement toujours le faire en utilisant 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, de sorte 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 vos informations User, tel que app.UseMvc().

    app.UseAuthentication();
    

    Notez que cela n'entraînera pas l'émission de votre jeton avec 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 dans une vue partielle de votre page HTML en combinaison avec l'authentification au support seul dans .Net 4.5, vous pouvez maintenant utiliser un ViewComponent pour faire de même. C'est essentiellement le même que le code Action du contrôleur ci-dessus.

23
Matt DeKrey

Pour obtenir ce que vous décrivez, vous aurez besoin d’un serveur d’autorisation OAuth2/OpenID Connect et d’un middleware validant les jetons d’accès pour votre API. Auparavant, Katana offrait un OAuthAuthorizationServerMiddleware, mais il n’existe plus dans ASP.NET Core.

Je suggère de jeter un coup d'œil à AspNet.Security.OpenIdConnect.Server , un fork expérimental du middleware du serveur d'autorisations OAuth2 utilisé par le tutoriel que vous avez mentionné: il existe une version OWIN/Katana 3 et une version ASP.NET Core prenant en charge les deux net451 (Bureau .NET) et netstandard1.4 (compatible avec .NET Core).

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

Ne manquez pas l'exemple MVC Core qui montre comment configurer un serveur d'autorisation OpenID Connect à l'aide de AspNet.Security.OpenIdConnect.Server et comment valider le fichier chiffré. jetons d'accès émis par le middleware de serveur: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Mvc/Mvc.Server/Startup.cs

Vous pouvez également lire ce billet de blog, qui explique comment implémenter l’octroi de mot de passe du propriétaire de la ressource, équivalent de l’authentification de base OAuth2: http://kevinchalet.com/2016/13/creating-your- own-openid-connect-server-with-asos-implémenter-la-ressource-propriétaire-mot-de-passe-identifiants-subvention /

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication();
    }

    public void Configure(IApplicationBuilder app)
    {
        // Add a new middleware validating the encrypted
        // access tokens issued by the OIDC server.
        app.UseOAuthValidation();

        // Add a new middleware issuing tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.TokenEndpointPath = "/connect/token";

            // Override OnValidateTokenRequest to skip client authentication.
            options.Provider.OnValidateTokenRequest = context =>
            {
                // Reject the token requests that don't use
                // grant_type=password or grant_type=refresh_token.
                if (!context.Request.IsPasswordGrantType() &&
                    !context.Request.IsRefreshTokenGrantType())
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
                        description: "Only grant_type=password and refresh_token " +
                                     "requests are accepted by this 
                    return Task.FromResult(0);
                }

                // Since there's only one application and since it's a public client
                // (i.e a client that cannot keep its credentials private),
                // call Skip() to inform the server the request should be
                // accepted without enforcing client authentication.
                context.Skip();

                return Task.FromResult(0);
            };

            // Override OnHandleTokenRequest to support
            // grant_type=password token requests.
            options.Provider.OnHandleTokenRequest = context =>
            {
                // Only handle grant_type=password token requests and let the
                // OpenID Connect server middleware handle the other grant types.
                if (context.Request.IsPasswordGrantType())
                {
                    // Do your credentials validation here.
                    // Note: you can call Reject() with a message
                    // to indicate that authentication failed.

                    var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
                    identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");

                    // By default, claims are not serialized
                    // in the access and identity tokens.
                    // Use the overload taking a "destinations"
                    // parameter to make sure your claims
                    // are correctly inserted in the appropriate tokens.
                    identity.AddClaim("urn:customclaim", "value",
                        OpenIdConnectConstants.Destinations.AccessToken,
                        OpenIdConnectConstants.Destinations.IdentityToken);

                    var ticket = new AuthenticationTicket(
                        new ClaimsPrincipal(identity),
                        new AuthenticationProperties(),
                        context.Options.AuthenticationScheme);

                    // Call SetScopes with the list of scopes you want to grant
                    // (specify offline_access to issue a refresh token).
                    ticket.SetScopes("profile", "offline_access");

                    context.Validate(ticket);
                }

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

project.json

{
  "dependencies": {
    "AspNet.Security.OAuth.Validation": "1.0.0",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0"
  }
}

Bonne chance!

4
Pinpoint

Vous pouvez utiliser OpenIddict pour servir les jetons (connexion), puis utiliser UseJwtBearerAuthentication pour les valider lors de l'accès à un API/contrôleur.

C’est essentiellement toute la configuration dont vous avez besoin dans Startup.cs:

ConfigureServices:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    // this line is added for OpenIddict to plug in
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

Configurez

app.UseOpenIddictCore(builder =>
{
    // here you 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;
});

// use jwt bearer authentication to validate the tokens
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    // must match the resource on your token request
    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<ApplicationUser, Application, ApplicationRole, string>.

Vous pouvez voir une explication complète (y compris le rapport de fonctionnement de github) sur ce billet de blog: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

3
Sean

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:

app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), 
      branch => {
                branch.UseJwtBearerAuthentication(options => {
                    options.AutomaticAuthenticate = true;
                    options.AutomaticChallenge = true;
                    options.RequireHttpsMetadata = false;
                    options.Audience = "localhost:54540";
                    options.Authority = "localhost:54540";
                });
    });

La logique dans /Providers/AuthorizationProvider.cs et le RessourceController de ce projet vaut également la peine d'être examinée;).

De plus, j'ai implémenté une application à une seule page avec une implémentation d'authentification basée sur un jeton en utilisant l'infrastructure frontale Aurelia et le 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

2
Darxtar