Configurar el punto final del servidor de autorización

Pregunta

¿Cómo usamos un token de portador con ASP.NET 5 usando un flujo de nombre de usuario y contraseña? Para nuestro escenario, queremos permitir que un usuario se registre e inicie sesión utilizando llamadas AJAX sin la necesidad de utilizar un inicio de sesión externo.

Para hacer esto, necesitamos tener un punto final de servidor de autorización. En las versiones anteriores de ASP.NET , haríamos lo siguiente y luego iniciar sesión en la URL ourdomain.com/Token .

 // Configure the application for OAuth based flow PublicClientId = "self"; OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/Token"), Provider = new ApplicationOAuthProvider(PublicClientId), AccessTokenExpireTimeSpan = TimeSpan.FromDays(14) }; 

Sin embargo, en la versión actual de ASP.NET, lo anterior no funciona. Hemos estado tratando de descubrir el nuevo enfoque. El ejemplo aspnet / identity en GitHub, por ejemplo, configura la autenticación de Facebook, Google y Twitter, pero no parece configurar un punto final del servidor de autorización OAuth no externo, a menos que eso sea lo que hace AddDefaultTokenProviders() , en cuyo caso nos preguntamos qué URL al proveedor sería.

Investigación

Hemos aprendido al leer la fuente aquí que podemos agregar “middleware de autenticación de portador” a la IAppBuilder.UseOAuthBearerAuthentication HTTP llamando a IAppBuilder.UseOAuthBearerAuthentication en nuestra clase de Startup . Este es un buen comienzo, aunque todavía no estamos seguros de cómo establecer su punto final token. Esto no funcionó:

 public void Configure(IApplicationBuilder app) { app.UseOAuthBearerAuthentication(options => { options.MetadataAddress = "meta"; }); // if this isn't here, we just get a 404 app.Run(async context => { await context.Response.WriteAsync("Hello World."); }); } 

Al ir a ourdomain.com/meta , solo recibimos nuestra página hello world.

La investigación adicional mostró que también podemos usar el método de extensión IAppBuilder.UseOAuthAuthentication , y que toma un parámetro OAuthAuthenticationOptions . Ese parámetro tiene una propiedad TokenEndpoint . Aunque no estamos seguros de lo que estamos haciendo, intentamos esto, lo que por supuesto no funcionó.

 public void Configure(IApplicationBuilder app) { app.UseOAuthAuthentication("What is this?", options => { options.TokenEndpoint = "/token"; options.AuthorizationEndpoint = "/oauth"; options.ClientId = "What is this?"; options.ClientSecret = "What is this?"; options.SignInScheme = "What is this?"; options.AutomaticAuthentication = true; }); // if this isn't here, we just get a 404 app.Run(async context => { await context.Response.WriteAsync("Hello World."); }); } 

En otras palabras, al ir a ourdomain.com/token , no hay ningún error, solo hay una vez más nuestra página hello world.

De acuerdo, recapitulemos los diferentes middleware OAuth2 (y sus respectivas extensiones IAppBuilder ) que fueron ofrecidos por OWIN / Katana 3 y los que serán portados a ASP.NET Core :

  • app.UseOAuthBearerAuthentication / OAuthBearerAuthenticationMiddleware : su nombre no era demasiado obvio, pero era (y aún lo es, ya que se ha portado a ASP.NET Core) responsable de validar los tokens de acceso emitidos por el middleware del servidor OAuth2. Básicamente es la contraparte simbólica del middleware de cookies y se usa para proteger sus API. En ASP.NET Core, se ha enriquecido con las características opcionales de OpenID Connect (ahora es capaz de recuperar automáticamente el certificado de firma del servidor de OpenID Connect que emitió los tokens).

Nota: comenzando con ASP.NET Core beta8, ahora se llama app.UseJwtBearerAuthentication / JwtBearerAuthenticationMiddleware .

  • app.UseOAuthAuthorizationServer / OAuthAuthorizationServerMiddleware : como su nombre indica, OAuthAuthorizationServerMiddleware era un middleware del servidor de autorizaciones OAuth2 y se usó para crear y emitir tokens de acceso. Este middleware no se transferirá a ASP.NET Core : Servicio de Autorización OAuth en ASP.NET Core .

  • app.UseOAuthBearerTokens : esta extensión realmente no correspondía a un middleware y era simplemente un envoltorio de la app.UseOAuthAuthorizationServer y app.UseOAuthBearerAuthentication . Era parte del paquete de Identidad de ASP.NET y era solo una forma conveniente de configurar el servidor de autorización OAuth2 y el middleware de portador OAuth2 utilizado para validar los tokens de acceso en una sola llamada. No se transferirá a ASP.NET Core .

ASP.NET Core ofrecerá un nuevo middleware completo (y estoy orgulloso de decir que lo diseñé):

  • app.UseOAuthAuthentication / OAuthAuthenticationMiddleware : este nuevo middleware es un cliente interactivo genérico OAuth2 que se comporta exactamente como app.UseFacebookAuthentication o app.UseGoogleAuthentication pero que es compatible con prácticamente cualquier proveedor OAuth2 estándar, incluido el suyo. Los proveedores de Google, Facebook y Microsoft han sido actualizados para heredar de este nuevo middleware básico.

Por lo tanto, el middleware que está buscando en realidad es el middleware del servidor de autorización OAuth2 , también OAuthAuthorizationServerMiddleware como OAuthAuthorizationServerMiddleware .

Aunque es considerado como un componente esencial por una gran parte de la comunidad, no será portado a ASP.NET Core .

Afortunadamente, ya hay un reemplazo directo: AspNet.Security.OpenIdConnect.Server ( https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server )

Este middleware es un fork avanzado del middleware del servidor de autorizaciones OAuth2 que viene con Katana 3 pero que apunta a OpenID Connect (que a su vez está basado en OAuth2). Utiliza el mismo enfoque de bajo nivel que ofrece un control detallado (a través de varias notificaciones) y le permite usar su propio marco (Nancy, ASP.NET Core MVC) para servir sus páginas de autorización como lo haría con el middleware del servidor OAuth2 . Configurarlo es fácil:

ASP.NET Core 1.x:

 // Add a new middleware validating access tokens issued by the server. app.UseOAuthValidation(); // Add a new middleware issuing tokens. app.UseOpenIdConnectServer(options => { options.TokenEndpointPath = "/connect/token"; // Create your own `OpenIdConnectServerProvider` and override // ValidateTokenRequest/HandleTokenRequest to support the resource // owner password flow exactly like you did with the OAuth2 middleware. options.Provider = new AuthorizationProvider(); }); 

ASP.NET Core 2.x:

 // Add a new middleware validating access tokens issued by the server. services.AddAuthentication() .AddOAuthValidation() // Add a new middleware issuing tokens. .AddOpenIdConnectServer(options => { options.TokenEndpointPath = "/connect/token"; // Create your own `OpenIdConnectServerProvider` and override // ValidateTokenRequest/HandleTokenRequest to support the resource // owner password flow exactly like you did with the OAuth2 middleware. options.Provider = new AuthorizationProvider(); }); 

Hay una versión OWIN / Katana 3 y una versión ASP.NET Core que admite .NET Desktop y .NET Core.

No dude en probar la muestra de Postman para entender cómo funciona. Recomiendo leer la publicación de blog asociada , que explica cómo puede implementar el flujo de contraseñas del propietario del recurso.

Siéntete libre de hacerme un seguimiento si todavía necesitas ayuda. ¡Buena suerte!

Con la ayuda de @ Pinpoint, hemos conectado los rudimentos de una respuesta. Muestra cómo los componentes se conectan entre sí sin ser una solución completa.

Demo Fiddler

Con nuestra configuración de proyecto rudimentaria, pudimos realizar la siguiente solicitud y respuesta en Fiddler.

Solicitud

 POST http://localhost:50000/connect/token HTTP/1.1 User-Agent: Fiddler Host: localhost:50000 Content-Length: 61 Content-Type: application/x-www-form-urlencoded grant_type=password&username=my_username&password=my_password 

Respuesta

 HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Length: 1687 Content-Type: application/json;charset=UTF-8 Expires: -1 X-Powered-By: ASP.NET Date: Tue, 16 Jun 2015 01:24:42 GMT { "access_token" : "eyJ0eXAiOi ... 5UVACg", "expires_in" : 3600, "token_type" : "bearer" } 

La respuesta proporciona un token de portador que podemos usar para obtener acceso a la parte segura de la aplicación.

Estructura del proyecto

Esta es la estructura de nuestro proyecto en Visual Studio. Tuvimos que establecer su Properties > Debug > Port en 50000 para que actuara como el servidor de identidad que configuramos. Aquí están los archivos relevantes:

 ResourceOwnerPasswordFlow Providers AuthorizationProvider.cs project.json Startup.cs 

Startup.cs

Para la legibilidad, he dividido la clase de Startup en dos parciales.

Startup.ConfigureServices

Para los conceptos básicos, solo necesitamos AddAuthentication() .

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

Startup.Configure

 public partial class Startup { public void Configure(IApplicationBuilder app) { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); // Add a new middleware validating access tokens issued by the server. app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, Audience = "resource_server_1", Authority = "http://localhost:50000/", RequireHttpsMetadata = false }); // Add a new middleware issuing tokens. app.UseOpenIdConnectServer(options => { // Disable the HTTPS requirement. options.AllowInsecureHttp = true; // Enable the token endpoint. options.TokenEndpointPath = "/connect/token"; options.Provider = new AuthorizationProvider(); // Force the OpenID Connect server middleware to use JWT // instead of the default opaque/encrypted format. options.AccessTokenHandler = new JwtSecurityTokenHandler { InboundClaimTypeMap = new Dictionary(), OutboundClaimTypeMap = new Dictionary() }; // Register an ephemeral signing key, used to protect the JWT tokens. // On production, you'd likely prefer using a signing certificate. options.SigningCredentials.AddEphemeralKey(); }); app.UseMvc(); app.Run(async context => { await context.Response.WriteAsync("Hello World!"); }); } } 

AuthorizationProvider.cs

 public sealed class AuthorizationProvider : OpenIdConnectServerProvider { public override Task ValidateTokenRequest(ValidateTokenRequestContext 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 server."); 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 that the request should be accepted without // enforcing client authentication. context.Skip(); return Task.FromResult(0); } public override Task HandleTokenRequest(HandleTokenRequestContext context) { // Only handle grant_type=password token requests and let the // OpenID Connect server middleware handle the other grant types. if (context.Request.IsPasswordGrantType()) { // Validate the credentials here (eg using ASP.NET Core Identity). // You can call Reject() with an error code/description to reject // the request and return a message to the caller. var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique identifier]"); // 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 serialized 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 SetResources with the list of resource servers // the access token should be issued for. ticket.SetResources("resource_server_1"); // 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.OpenIdConnect.Server": "1.0.0", "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.0", } // other code omitted }