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

161

Je travaille avec l'application ASP.NET Core. J'essaye d'implémenter l'authentification basée sur les jetons mais je ne peux pas comprendre comment utiliser le nouveau système de sécurité pour mon cas. J'ai parcouru des 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 l' /tokenURL en passant le nom d'utilisateur et le mot de passe. WebApi doit autoriser l'utilisateur et renvoyer access_tokence qui sera utilisé par l'application angularjs dans les demandes suivantes.

J'ai trouvé un excellent article sur la mise en œuvre exactement 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 qu'elle fonctionne avec l'authentification basée sur les jetons?

Subvention
la source
J'ai le même problème et je prévoyais de faire tout cela moi-même, pour info il y a une autre question stackoverflow.com/questions/29055477/... mais pas encore de réponse, voyons ce qui se passe
Son_of_Sam
Je suis également confronté au même problème mais je n'ai pas encore trouvé de solution ... Je dois écrire une authentification personnalisée à l'aide d'un autre service qui authentifie mon jeton.
Mayank Gupta

Réponses:

138

Mise à jour pour .Net Core 3.1:

David Fowler (architecte de l'équipe ASP .NET Core) a mis en place un ensemble incroyablement simple d'applications de tâches, y compris une application simple démontrant JWT . J'incorporerai bientôt ses mises à jour et son style simpliste à cet article.

Mis à 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 même code qui génère les jetons vérifie également les jetons. Cependant, si vous répartissez 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 Startup.cs ConfigureServices. Nous utiliserons l'injection de dépendances plus tard pour accéder à ces paramètres. Je suppose que votre authenticationConfigurationest un objet ConfigurationSectionou Configurationtel que vous pouvez avoir une configuration différente pour le débogage et la production. Assurez-vous de conserver 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 exactement synchronisées. Ce sont les seuls paramètres que vous devez modifier.

  3. Configurez l'authentification. Vous devriez avoir cette ligne avant tout middleware nécessitant vos Userinformations, tel que app.UseMvc().

    app.UseAuthentication();

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

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

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Voici la partie 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 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 directeur. Si vous utilisez Identity, vous pouvez utiliser IUserClaimsPrincipalFactory<>pour transformer votre Useren un ClaimsPrincipal.

  6. Pour tester : Obtenir un jeton, mettre en forme à jwt.io . Les instructions que j'ai fournies ci-dessus vous permettent également d'utiliser le secret de votre config pour valider la signature!

  7. Si vous l'avez rendu dans une vue partielle sur votre page HTML en combinaison avec l'authentification au porteur uniquement dans .Net 4.5, vous pouvez maintenant utiliser a ViewComponentpour faire de même. C'est essentiellement le même que le code d'action du contrôleur ci-dessus.

Matt DeKrey
la source
1
Vous devrez réellement injecter IOptions<OAuthBearerAuthenticationOptions>pour utiliser les options; l'utilisation directe d'un objet Options n'est pas prise en charge en raison de la configuration nommée prise en charge par l'infrastructure du modèle d'options.
Matt DeKrey
2
Mis à jour à ce que j'utilise, mais maintenant la réponse devrait être réécrite. Merci de m'avoir poussé!
Matt DeKrey
5
Le n ° 5 a depuis été remplacé par ce qui suit dans Microsoft.AspNet.Authentication.OAuthBearer - beta 5-6 et peut-être des versions bêta antérieures, mais ne les a pas confirmées. auth.AddPolicy ("Bearer", nouveau AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk
5
@MattDeKrey J'ai utilisé cette réponse comme point de départ pour un exemple d'authentification simple basée sur un jeton, et je l'ai mise à jour pour qu'elle fonctionne avec la version bêta 7 - voir github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - intègre également quelques-uns des pointeurs de ces commentaires.
Mark Hughes
2
Mise à jour à nouveau pour RC1 - anciennes versions pour Beta7 et Beta8 disponibles dans les branches sur GitHub.
Mark Hughes
83

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

Générez une clé pour votre application

Dans mon exemple, je génère une clé aléatoire à chaque fois que l'application démarre, vous devrez en générer une et la stocker quelque part et la fournir à votre application. Consultez ce fichier pour savoir comment je génère une clé aléatoire et comment vous pouvez l'importer à partir d'un fichier .json . Comme suggéré dans les commentaires de @kspearrin, l' API de protection des données semble être un candidat idéal pour gérer les clés "correctement", mais je n'ai pas encore déterminé si c'est possible. Veuillez soumettre une demande d'extraction si vous y parvenez!

Startup.cs - ConfigureServices

Ici, nous devons charger une clé privée avec laquelle nos jetons doivent être signés, 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 keyque nous réutiliserons dans la méthode Configure ci-dessous. TokenAuthOptions est une classe simple qui contient l'identité de signature, l'audience et l'émetteur dont nous aurons besoin dans le 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 mis en place une politique d'autorisation pour nous permettre d'utiliser [Authorize("Bearer")]sur les points de terminaison et les classes que nous souhaitons protéger.

Startup.cs - Configurer

Ici, nous devons configurer le 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 jetons, 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 point de terminaison de connexion, dans mon exemple, je prends un nom d'utilisateur et un mot de passe et je valide ceux à l'aide d'une instruction if, mais la chose clé que vous devez faire est de créer ou de charger une revendication -based identité et générer le jeton pour cela:

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")]à n'importe quelle méthode ou classe que vous souhaitez protéger, et vous devriez obtenir une erreur si vous essayez d'y accéder sans jeton présent. Si vous souhaitez 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 .

Mark Hughes
la source
1
C'est un très bon exemple, et il comprenait toutes les pièces manquantes dont j'avais besoin pour que l'exemple de @ MattDeKrey fonctionne, merci beaucoup! Notez que quiconque cible toujours beta7 au lieu de beta8 peut toujours trouver cet exemple dans l' historique de github
nickspoon
1
Heureux que cela ait aidé @nickspoon - si vous avez des problèmes avec cela, faites-le moi savoir ou inscrivez une pull request sur github et je mettrai à jour!
Mark Hughes
2
Merci pour cela, mais je ne comprends pas très bien pourquoi quelque chose qui a fonctionné hors de la boîte dans l'API Web ASP.Net 4 nécessite maintenant un peu de configuration dans ASP.Net 5. Cela ressemble à un pas en arrière.
JMK
1
Je pense qu'ils poussent vraiment "l'authentification sociale" pour ASP.NET 5, ce qui a du sens je suppose, mais il y a des applications qui ne conviennent pas, donc je ne suis pas sûr d'être d'accord avec leur direction @JMK
Mark Hughes
1
@YuriyP Je dois mettre à jour cette réponse pour RC2 - Je n'ai pas encore mis à jour notre application interne qui l'utilise en RC2, donc je ne suis pas sûr de ce qui est impliqué. Je mettrai à jour une fois que j'ai travaillé la différence ...
Mark Hughes
3

Vous pouvez consulter les exemples de connexion OpenId qui illustrent comment gérer différents mécanismes d'authentification, y compris les jetons JWT:

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

Si vous regardez 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 dans /Providers/AuthorizationProvider.cs et le RessourceController de ce projet valent également la peine d'être examinés;).

Vous pouvez également utiliser le code suivant pour valider les jetons (il existe également un extrait de code 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 d'une seule page avec une implémentation d'authentification basée sur des jetons à l'aide du framework frontal Aurelia et du noyau ASP.NET. Il existe également un signal R de connexion persistante. Cependant, je n'ai fait aucune implémentation DB. Le code peut être vu ici: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

J'espère que cela t'aides,

Meilleur,

Alex

Darxtar
la source
1

Jetez un œil à OpenIddict - c'est un nouveau projet (au moment de la rédaction) qui facilite la configuration de la création de jetons JWT et l'actualisation des jetons dans ASP.NET 5. La validation des jetons est gérée par d'autres logiciels.

En supposant que vous utilisez IdentityavecEntity Framework , la dernière ligne est ce que vous ajouteriez à votre ConfigureServicesméthode:

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 y a une ou deux autres choses mineures, telles que votre DbContext doit dériver d'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 sur: https://github.com/capesean/openiddict-test

Sean
la source