Autenticación basada en tokens en ASP.NET Core


161

Estoy trabajando con la aplicación ASP.NET Core. Estoy tratando de implementar la autenticación basada en tokens, pero no puedo entender cómo usar el nuevo sistema de seguridad para mi caso. Revisé ejemplos, pero no me ayudaron mucho, están utilizando autenticación de cookies o autenticación externa (GitHub, Microsoft, Twitter).

Cuál es mi escenario: la aplicación angularjs debe solicitar el /tokenpaso del nombre de usuario y contraseña de la URL WebApi debe autorizar al usuario y devolver lo access_tokenque será utilizado por la aplicación angularjs en las siguientes solicitudes.

Encontré un excelente artículo sobre la implementación de exactamente lo que necesito en la versión actual de ASP.NET: autenticación basada en tokens utilizando ASP.NET Web API 2, Owin e Identity . Pero 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 WebApi para que funcione con autenticación basada en token?


Tengo el mismo problema y estaba planeando hacer todo eso yo mismo. Para su información, hay otra pregunta stackoverflow.com/questions/29055477/… pero todavía no respondo, veamos qué sucede
Son_of_Sam


También estoy enfrentando el mismo problema, pero aún no pude encontrar una solución ... Necesito escribir una autenticación personalizada utilizando otro servicio que autentique mi token.
Mayank Gupta

Respuestas:


137

Actualización para .Net Core 3.1:

David Fowler (arquitecto del equipo de ASP .NET Core) ha reunido un conjunto increíblemente simple de aplicaciones de tareas, incluida una aplicación simple que demuestra JWT . Pronto incorporaré sus actualizaciones y estilo simplista a esta publicación.

Actualizado para .Net Core 2:

Las versiones anteriores de esta respuesta usaban RSA; Realmente no es necesario si el mismo código que genera los tokens también verifica los tokens. Sin embargo, si está distribuyendo la responsabilidad, es probable que aún desee hacerlo utilizando una instancia de Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Crea algunas constantes que usaremos más adelante; esto es lo que hice:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
  2. Agregue esto a su Startup.cs's ConfigureServices. Usaremos la inyección de dependencia más tarde para acceder a esta configuración. Supongo que authenticationConfigurationes un objeto ConfigurationSectionu Configurationtal que puede tener una configuración diferente para la depuración y la producción. ¡Asegúrese de guardar su clave de forma segura! Puede ser cualquier cadena.

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

    He visto otras respuestas cambiar otras configuraciones, como ClockSkew; los valores predeterminados están configurados de tal manera que deberían funcionar para entornos distribuidos cuyos relojes no están exactamente sincronizados. Estas son las únicas configuraciones que necesita cambiar.

  3. Configurar autenticación. Debería tener esta línea antes de cualquier middleware que requiera su Userinformación, como app.UseMvc().

    app.UseAuthentication();

    Tenga en cuenta que esto no hará que su token se emita con el SignInManagero cualquier otra cosa. Deberá proporcionar su propio mecanismo para generar su JWT; consulte a continuación.

  4. Es posible que desee especificar un AuthorizationPolicy. Esto le permitirá especificar controladores y acciones que solo permiten tokens de portador como autenticación utilizando [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Aquí viene la parte difícil: construir el token.

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

    Luego, en su controlador donde desea su token, algo como lo siguiente:

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

    Aquí, supongo que ya tienes un director. Si está utilizando Identity, puede usarlo IUserClaimsPrincipalFactory<>para transformarlo Useren a ClaimsPrincipal.

  6. Para probarlo : Obtener una ficha, ponerlo en la forma en jwt.io . ¡Las instrucciones que proporcioné anteriormente también le permiten usar el secreto de su configuración para validar la firma!

  7. Si estaba renderizando 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 a ViewComponentpara hacer lo mismo. Es casi lo mismo que el código de acción del controlador anterior.


1
Tendrá que inyectar IOptions<OAuthBearerAuthenticationOptions>para usar las Opciones; el uso de un objeto Opciones directamente no es compatible debido a la configuración con nombre que es compatible con el marco del Modelo de opciones.
Matt DeKrey

2
Actualizado a lo que estoy usando, aunque ahora la respuesta debería ser reescrita. Gracias por meterme!
Matt DeKrey

55
El # 5 se ha cambiado a lo siguiente en Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6 y posiblemente versiones beta anteriores, pero no lo han confirmado. auth.AddPolicy ("Bearer", new AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk

55
@MattDeKrey He usado esta respuesta como punto de partida para un ejemplo de autenticación basada en token simple, y la actualicé para que funcione con la versión beta 7; consulte github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample , también incorpora algunos de los indicadores de estos comentarios.
Mark Hughes

2
Actualizada nuevamente para RC1 - versiones anteriores para Beta7 y Beta8 disponibles en sucursales en GitHub.
Mark Hughes

83

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

Genere una clave para su 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. Vea este archivo para saber cómo estoy generando una clave aleatoria y cómo podría importarla desde un archivo .json . Como se sugiere en los comentarios de @kspearrin, la API de protección de datos parece un candidato ideal para administrar las claves "correctamente", pero aún no he resuelto si eso es posible. ¡Envíe una solicitud de extracción si lo resuelve!

Startup.cs - ConfigureServices

Aquí, necesitamos 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 variable de nivel de clase keyque reutilizaremos en el método de configuración a continuación. TokenAuthOptions es una clase simple que contiene la identidad de firma, la audiencia y el emisor que necesitaremos en 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<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());
});

También hemos establecido una política de autorización que nos permite usar [Authorize("Bearer")]en los puntos finales y 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 - 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

En el controlador de token, debe tener un método para generar claves firmadas utilizando la clave que se cargó en Startup.cs. Hemos registrado una instancia de TokenAuthOptions en Startup, por lo que debemos inyectarla en el constructor de TokenController:

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

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

Luego, deberá generar el token en su controlador para el punto final de inicio de sesión, en mi ejemplo, estoy tomando un nombre de usuario y contraseña y validando los que usan una instrucción if, pero lo clave que debe 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; }
}

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

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, deberá registrar un controlador de excepción personalizado como lo he hecho en mi ejemplo aquí .


1
Este es un ejemplo realmente excelente, e incluyó todas las piezas que faltaban para que el ejemplo de @ MattDeKrey funcionara, ¡muchas gracias! Tenga en cuenta que cualquiera que todavía apunte a beta7 en lugar de beta8 aún puede encontrar ese ejemplo en la historia de
github

1
Me alegro de que haya ayudado a @nickspoon: si tiene algún problema, hágamelo saber o envíe una solicitud de extracción en github y lo actualizaré.
Mark Hughes

2
Gracias por esto, sin embargo, no entiendo por qué algo que salió de la caja en la API web ASP.Net 4 ahora requiere bastante configuración en ASP.Net 5. Parece un paso atrás.
JMK

1
Creo que realmente están impulsando la "autenticación social" para ASP.NET 5, lo que tiene sentido, supongo, pero hay aplicaciones que no son apropiadas, así que no estoy seguro de estar de acuerdo con su dirección @JMK
Mark Hughes

1
@YuriyP Necesito actualizar esta respuesta para RC2: aún no he actualizado nuestra aplicación interna que usa esto para RC2, así que no estoy seguro de lo que implica. Actualizaré una vez que haya resuelto la diferencia ...
Mark Hughes

3

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

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

Si observa el proyecto Cordova Backend, la configuración de la API es la siguiente:

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

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

Alternativamente, también puede usar el siguiente código para validar los tokens (también hay un fragmento para que funcione con 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);
                }
            };
        });

Para emitir un token, puede usar los paquetes del servidor openId Connect de la siguiente manera:

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

EDITAR: he implementado una aplicación de una sola página con implementación de autenticación basada en token utilizando el marco front-end de Aurelia y el núcleo ASP.NET. También hay una señal R de conexión persistente. 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


1

Eche un vistazo a OpenIddict: es un nuevo proyecto (en el momento de la escritura) que facilita la configuración de la creación de tokens JWT y la actualización de tokens en ASP.NET 5. La validación de los tokens es manejada por otro software.

Suponiendo que el uso Identityde Entity Frameworkla última línea es lo que se agrega a su ConfigureServicesmétodo:

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

En Configure, configura OpenIddict para servir tokens 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;
});

También configura la validación de tokens en 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/";
});

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

Puedes ver una explicación completa en esta publicación de blog: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Una demostración funcional está disponible en: https://github.com/capesean/openiddict-test

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.