Search code examples
authenticationoauth-2.0asp.net-coreopenid-connectaspnet-contrib

How to Consume/Validate Token Issued by AspNet.Security.OpenIdConnect.Server (RC1)?


I have followed everything I know from posts regarding how to implement AspNet.Security.OpenIdConnect.Server.

Pinpoint, do you hear me? ;)

I've managed to separate token issuing and token consumption. I won't show the "auth server side" because I think that part is all set, but I'll show how I built the authentication ticket inside my custom AuthorizationProvider:

public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
    // The other overrides are not show. I've relaxed them to always validate.

    public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
    {
          // I'm using Microsoft.AspNet.Identity to validate user/password. 
          // So, let's say that I already have MyUser user from
          //UserManager<MyUser> UM:

            var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
            //identity.AddClaims(await UM.GetClaimsAsync(user));
            identity.AddClaim(ClaimTypes.Name, user.UserName);

            (await UM.GetRolesAsync(user)).ToList().ForEach(role => {
                identity.AddClaim(ClaimTypes.Role, role);
            });

            var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
                                                  new AuthenticationProperties(),
                                                  context.Options.AuthenticationScheme);
            // Some new stuff, per my latest research
            ticket.SetResources(new[] { "my_resource_server" });
            ticket.SetAudiences(new[] { "my_resource_server" });
            ticket.SetScopes(new[] { "defaultscope" });

            context.Validated(ticket);
        }
    }

And startup at the auth server:

using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;

using MyAuthServer.Providers;

namespace My.AuthServer
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication();
            services.AddCaching();
            services.AddMvc();

            string connectionString = "there is actually one";

            services.AddEntityFramework()
                    .AddSqlServer()
                    .AddDbContext<MyDbContext>(options => {
                        options.UseSqlServer(connectionString).UseRowNumberForPaging();
                    });

            services.AddIdentity<User, Role>()
                    .AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseIISPlatformHandler();

            app.UseOpenIdConnectServer(options => {
                options.ApplicationCanDisplayErrors = true;
                options.AllowInsecureHttp = true;                
                options.Provider = new AuthorizationProvider();
                options.TokenEndpointPath = "/token";
                options.AccessTokenLifetime = new TimeSpan(1, 0, 0, 0);
                options.Issuer = new Uri("http://localhost:60556/");
            });

            app.UseMvc();
            app.UseWelcomePage();
        }

        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.

POST /token HTTP/1.1
Host: localhost:60556
Content-Type: application/x-www-form-urlencoded

username=admin&password=pw&grant_type=password

Now, At the resource server side, I'm using JWT Bearer Authentication. On startup, I've got:

using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;

namespace MyResourceServer
{
    public class Startup
    {            
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            string connectionString = "there is actually one";

            services.AddEntityFramework()
                    .AddSqlServer()
                    .AddDbContext<MyDbContext>(options => {
                        options.UseSqlServer(connectionString).UseRowNumberForPaging();                        
                    });

            services.AddIdentity<User, Role>()
                    .AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseIISPlatformHandler();
            app.UseMvc();
            app.UseWelcomePage();

            app.UseJwtBearerAuthentication(options => {
                options.Audience = "my_resource_server";  
                options.Authority = "http://localhost:60556/"; 
                options.AutomaticAuthenticate = true;
                options.RequireHttpsMetadata = false;                
            });
        }

        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

When I make this HTTP request to the resource server, I get a 401 Unauthorized:

GET /api/user/myroles HTTP/1.1
Host: localhost:64539
Authorization: Bearer eyJhbGciOiJS...
Content-Type: application/json;charset=utf-8

The controller who has a route to /api/user/myroles is decorated with a plain [Authorize] with no parameters.

I feel like I'm missing something in both auth and resource servers, but don't know what they are.

The other questions that ask "how to validate token issued by AspNet.Security.OpenIdConnect.Server" don't have an answer. I would appreciate some help in this.

Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.


UPDATE I've included both of my startup.cs, from each of auth and resource servers. Could there be anything wrong that would cause the resource server to always return a 401 for every request?

One thing I didn't really touch throughout this whole endeavor is signing. It seems to generate a signature for the JWT at the auth server, but the resource server (I guess) doesn't know the signing keys. Back in the OWIN projects, I had to create a machine key and put on the two servers.


Solution

  • Edit: the order of your middleware instances is not correct: the JWT bearer middleware must be registered before MVC:

    app.UseIISPlatformHandler();
    
    app.UseJwtBearerAuthentication(options => {
        options.Audience = "my_resource_server";  
        options.Authority = "http://localhost:60556/"; 
        options.AutomaticAuthenticate = true;
        options.RequireHttpsMetadata = false;
    });
    
    app.UseMvc();
    app.UseWelcomePage();
    

    Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.

    Your authorization server and resource server configuration look fine, but you're not setting the "destination" when adding your claims (don't forget that to avoid leaking confidential data, AspNet.Security.OpenIdConnect.Server refuses to serialize the claims that don't explicitly specify a destination):

    var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
    identity.AddClaim(ClaimTypes.Name, user.UserName, destination: "id_token token");
    
    (await UM.GetRolesAsync(user)).ToList().ForEach(role => {
        identity.AddClaim(ClaimTypes.Role, role, destination: "id_token token");
    });
    

    Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.

    Starting with the next beta (ASOS beta5, not yet on NuGet.org when writing this answer), we'll stop using JWT as the default format for access tokens, but of course, JWT will still be supported OTB.

    Tokens now being opaque by default, you'll have to use either the new validation middleware (inspired from Katana's OAuthBearerAuthenticationMiddleware) or the new standard introspection middleware, that implements the OAuth2 introspection RFC:

    app.UseOAuthValidation();
    
    // Alternatively, you can also use the introspection middleware.
    // Using it is recommended if your resource server is in a
    // different application/separated from the authorization server.
    // 
    // app.UseOAuthIntrospection(options => {
    //     options.AutomaticAuthenticate = true;
    //     options.AutomaticChallenge = true;
    //     options.Authority = "http://localhost:54540/";
    //     options.Audience = "resource_server";
    //     options.ClientId = "resource_server";
    //     options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
    // });
    

    You can find more information about these 2 middleware here: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/185