Autenticación basada en tokens en ASP.NET Core (actualizado)

Estoy trabajando con la aplicación ASP.NET Core. Estoy intentando implementar la autenticación basada en tokens, pero no puedo descubrir cómo usar el nuevo sistema de seguridad .

Mi escenario: un cliente solicita un token. Mi servidor debe autorizar al usuario y devolver access_token, que será utilizado por el cliente en las siguientes solicitudes.

Aquí hay dos excelentes artículos sobre cómo implementar exactamente lo que necesito:

  • Autenticación basada en tokens utilizando ASP.NET Web API 2, Owin e Identity
  • Uso de tokens web JSON

El problema es que no es obvio para mí cómo hacer lo mismo en ASP.NET Core.

Mi pregunta es: ¿cómo configurar la aplicación ASP.NET Core Web Api para trabajar con autenticación basada en token? ¿Qué dirección debería seguir? ¿Has escrito algún artículo sobre la versión más nueva o sabes dónde puedo encontrar algunos?

¡Gracias!

Trabajando a partir de la fabulosa respuesta de Matt Dekrey , he creado un ejemplo completamente funcional de autenticación basada en tokens, trabajando contra ASP.NET Core (1.0.1). Puede encontrar el código completo en este repository en GitHub (twigs alternativas para 1.0.0-rc1 , beta8 , beta7 ), pero en resumen, los pasos importantes son:

Genera una clave para tu aplicación

En mi ejemplo, genero una clave aleatoria cada vez que se inicia la aplicación, tendrá que generar una y almacenarla en algún lugar y proporcionarla a su aplicación. Consulte este archivo para saber cómo estoy generando una clave aleatoria y cómo puede importarla desde un archivo .json . Como se sugiere en los comentarios de @kspearrin, la API de protección de datos parece ser un candidato ideal para administrar las claves “correctamente”, pero aún no he resuelto si eso es posible. Por favor envíe una solicitud de extracción si lo resuelve!

Startup.cs – ConfigureServices

Aquí, debemos cargar una clave privada para que se firmen nuestros tokens, que también usaremos para verificar los tokens a medida que se presentan. Estamos almacenando la clave en una key variable de nivel de clase que volveremos a utilizar en el método Configure a continuación. TokenAuthOptions es una clase simple que contiene la identidad de firma, la audiencia y el emisor que necesitaremos en el TokenController para crear nuestras claves.

 // 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(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()); }); 

También hemos establecido una política de autorización que nos permite usar [Authorize("Bearer")] en los puntos finales y las clases que deseamos proteger.

Startup.cs – Configurar

Aquí, necesitamos configurar 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 - ie // 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

En el controlador de token, debe tener un método para generar claves firmadas utilizando la clave que se cargó en Startup.cs. Registramos una instancia de TokenAuthOptions en Inicio, por lo que necesitamos inyectar eso en el constructor de TokenController:

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

Entonces necesitarás generar el token en tu controlador para el punto final de inicio de sesión, en mi ejemplo tomo un nombre de usuario y contraseña y validarlos usando una statement if, pero la clave que debes hacer es crear o cargar un reclamo basada en identidad y generar el token para eso:

 public class AuthRequest { public string username { get; set; } public string password { get; set; } } ///  /// Request a new token for a given username/password pair. ///  ///  ///  [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); } 

Y eso debería ser. Simplemente agregue [Authorize("Bearer")] a cualquier método o clase que desee proteger, y debería obtener un error si intenta acceder sin un token presente. Si desea devolver un error 401 en lugar de 500, necesitará registrar un manejador de excepción personalizado como lo hice en mi ejemplo aquí .

  1. Genere una clave RSA solo para su aplicación. Un ejemplo muy básico se encuentra a continuación, pero hay mucha información sobre cómo se manejan las claves de seguridad en .Net Framework; Le recomiendo que lea algo de eso , al menos.

     private static string GenerateRsaKeys() { RSACryptoServiceProvider myRSA = new RSACryptoServiceProvider(2048); RSAParameters publicKey = myRSA.ExportParameters(true); return myRSA.ToXmlString(includePrivateParameters: true); } 

    Guarde esto en un archivo .xml e inclúyalo con su aplicación; Lo incrustó en mi archivo DLL porque es un pequeño proyecto personal, pensé que nadie debería tener acceso a mi ensamblaje de todos modos, pero hay muchas razones por las cuales esta no es una buena idea, así que no proporciono ese ejemplo aquí. En última instancia, debe decidir qué es lo mejor para su proyecto.

    Nota: Se señaló que ToXmlString y FromXmlString no están disponibles en .NET Core. En su lugar, puede guardar / cargar los valores usted mismo utilizando RSAParameters ExportParameters(bool includePrivateParameters) y void ImportParameters(RSAParameters parameters) de forma compatible con Core, como el uso de JSON.

  2. Cree algunas constantes que usaremos más tarde; esto es lo que hice:

     const string TokenAudience = "Myself"; const string TokenIssuer = "MyProject"; 
  3. Agregue esto a ConfigureServices su Startup.cs. Usaremos la dependency injection más tarde para acceder a estas configuraciones. Me estoy quedando sin acceso a la secuencia RSA xml; pero asumo que tienes acceso a ella en una variable de stream .

     RsaSecurityKey key; using (var textReader = new System.IO.StreamReader(stream)) { RSACryptoServiceProvider publicAndPrivate = new RSACryptoServiceProvider(); publicAndPrivate.FromXmlString(textReader.ReadToEnd()); key = new RsaSecurityKey(publicAndPrivate.ExportParameters(true)); } services.AddInstance(new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest)); services.Configure(bearer => { bearer.TokenValidationParameters.IssuerSigningKey = key; bearer.TokenValidationParameters.ValidAudience = TokenAudience; bearer.TokenValidationParameters.ValidIssuer = TokenIssuer; }); 
  4. Configurar la Autenticación del portador. Si está utilizando Identity, haga esto antes de la línea UseIdentity . Tenga en cuenta que cualquier línea de autenticación de terceros, como UseGoogleAuthentication , debe ir antes que la línea UseIdentity . No necesita ninguna UseCookieAuthentication si está utilizando Identity.

     app.UseOAuthBearerAuthentication(); 
  5. Es posible que desee especificar una AuthorizationPolicy . Esto le permitirá especificar controladores y acciones que solo permitan tokens de portador como autenticación mediante [Authorize("Bearer")] .

     services.ConfigureAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationTypes(OAuthBearerAuthenticationDefaults.AuthenticationType) .RequireAuthenticatedUser().Build()); }); 
  6. Aquí viene la parte difícil: construir el token. No voy a proporcionar todo mi código aquí, pero debería ser suficiente para reproducirlo. (Tengo algunas cosas patentadas no relacionadas alrededor de este código en mi propia base de código).

    Este bit se inyecta desde el constructor; esta es la razón por la que configuramos las opciones anteriores en lugar de simplemente pasarlas a UseOAuthBearerAuthentication ()

     private readonly OAuthBearerAuthenticationOptions bearerOptions; private readonly SigningCredentials signingCredentials; 

    Luego, en tu /Token Acción /Token

     // add to using clauses: // using System.IdentityModel.Tokens.Jwt; var handler = bearerOptions.SecurityTokenValidators.OfType() .First(); // The identity here is the ClaimsIdentity you want to authenticate the user as. // You can add your own custom claims to it if you like. // You can get this using the SignInManager if you're using Identity. var securityToken = handler.CreateToken( issuer: bearerOptions.TokenValidationParameters.ValidIssuer, audience: bearerOptions.TokenValidationParameters.ValidAudience, signingCredentials: signingCredentials, subject: identity); var token = handler.WriteToken(securityToken); 

    El var token es el var token su portador; puede devolverlo como una cadena para que el usuario pase como se espera para la autenticación de portador.

  7. Si estaba representando esto en una vista parcial en su página HTML en combinación con la autenticación de solo portador en .Net 4.5, ahora puede usar un ViewComponent para hacer lo mismo. Es casi lo mismo que el código de acción del controlador anterior.

Para lograr lo que describes, necesitarás un servidor de autorización OAuth2 / OpenID Connect y un middleware que valide los tokens de acceso para tu API. Katana solía ofrecer un OAuthAuthorizationServerMiddleware , pero ya no existe en ASP.NET Core.

Sugiero echar un vistazo a AspNet.Security.OpenIdConnect.Server , una bifurcación experimental del middleware del servidor de autorización OAuth2 que se utiliza en el tutorial que mencionaste: hay una versión OWIN / Katana 3 y una versión ASP.NET Core que admite ambos net451 (.NET Desktop) y netstandard1.4 (compatible con .NET Core).

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

No se pierda el ejemplo de MVC Core que muestra cómo configurar un servidor de autorización de OpenID Connect utilizando AspNet.Security.OpenIdConnect.Server y cómo validar los tokens de acceso cifrados emitidos por el middleware del servidor: https://github.com/aspnet- contrib / AspNet.Security.OpenIdConnect.Server / blob / dev / samples / Mvc / Mvc.Server / Startup.cs

También puede leer esta publicación de blog, que explica cómo implementar la concesión de contraseña del propietario del recurso, que es el equivalente OAuth2 de autenticación básica: http://kevinchalet.com/2016/07/13/creating-your-own-openid- connect-server-with-asos-implementation-the-resource-owner-password-credentials-grant /

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 // (ie 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" } } 

¡Buena suerte!

Puede utilizar OpenIddict para servir los tokens (inicio de sesión) y luego usar UseJwtBearerAuthentication para validarlos cuando se UseJwtBearerAuthentication a un API / Controller.

Esta es esencialmente toda la configuración que necesita en Startup.cs :

ConfigureServices:

 services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders() // this line is added for OpenIddict to plug in .AddOpenIddictCore(config => config.UseEntityFramework()); 

Configurar

 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/"; }); 

Hay una o dos cosas menores, como su DbContext necesita derivar de OpenIddictContext .

Puedes ver una explicación completa (incluido el informe de funcionamiento de github) en esta entrada de blog mía: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Puede echar un vistazo a las muestras de conexión de OpenId que ilustran cómo tratar los diferentes mecanismos de autenticación, incluidos los tokens JWT:

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

Si nos fijamos en el proyecto Cordova Backend, la configuración para la API es así:

 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"; }); }); 

También vale la pena echarle un vistazo a la lógica en /Providers/AuthorizationProvider.cs y al RessourceController de ese proyecto;).

Además, he implementado una aplicación de una sola página con implementación de autenticación basada en token utilizando el framework de Aurelia front end y el núcleo de ASP.NET. También hay una señal de conexión persistente R. Sin embargo, no he hecho ninguna implementación de DB. El código se puede ver aquí: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Espero que esto ayude,

Mejor,

Alex