In this episode, we look at the group management service, and the changes required for it to enforce the requests authentication using an access token (JWT).

For the walk-through you can check out the next video, but if you prefer a quick read, skip to the written synthesis.

The playlist for the whole series is here.

Intro

In the last post, we’ve seen how to configure IdentityServer4 in the auth service. In this post, we take advantage of ASP.NET Core built-in features to authenticate requests to the group management API using JWT (JSON Web Tokens) provided by the auth service to a client application, after a successful authentication.

Configuring authentication and authorization

Configuring the application to require JWT authentication is rather simple, almost like magic 😛, because ASP.NET Core comes prepared to handle such scenarios out of the box. We’ll start by seeing how to perform these configurations, but later on we’ll try to shed some light on some of the magic.

Setting up JWT authentication

Configuring authentication will require the usual 2 steps in the Startup class (or methods called by it): configuring services and configuring the request handling pipeline.

Starting with the simplest part, the request handling pipeline, we just need to add app.UseAuthentication(), much like we did when configuring authentication in the auth service.

Now let’s move to the service configuration. We have a ServiceCollectionExtensions class to where we have been extracting the methods that configure the services, so we’ll create a new one named AddConfiguredAuth in there (although this should be refactored later, to avoid having all the unrelated configurations in the same place).

IoC\ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions
{
    // ...
    public static IServiceCollection AddConfiguredAuth(this IServiceCollection services)
    {
        services
            .AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = "https://localhost:5005";
                options.Audience = "GroupManagement";
            });

        return services;
    }
    // ...
}

Looking at the code above, I think it’s easy to see why I said it was simple to implement, 4 or 5 lines of code take care of everything (because ASP.NET Core implements the hard parts of course).

Let’s quickly go through what’s going on:

  • We call AddAuthentication to add the core required services for authentication, along with the default scheme we want to use for authentication.
  • We call AddJwtBearer to add the required services for JWT authentication. We provide the scheme name, so it can be matched by what we passed to AddAuthentication and also provide some options for handling the JWTs.
    • Authority is the url of the auth service, so the application can fetch some required information to validate the tokens.
    • Audience is used in the token validation to ensure the token was intended to be used by this specific application.

At first glance one might ask, how is this enough to validate the tokens? Even though it might not seem a lot, the Authority plays a huge role in this, as OpenId Connect specs include discovery of information about the provider, meaning the OpenId Connect provider exposes well known endpoints where the relying parties can fetch information. We’ll see an example in a coming section.

Setting up authorization based on the authenticated user

In episode 019 (and even on 016) we took a look at authorization in ASP.NET Core. What we need in this application is very similar.

Basically we require a user to be authenticated and have a scope named GroupManagement to be authorized to access the API. We could use different scopes for different endpoints, but no need to go down that path for now.

To perform these configurations we go to the ServiceCollectionExtensions.AddRequiredMvcComponents method and make some additions:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
    {
        // ...
        var mvcBuilder = services.AddMvcCore(options =>
        {
            // ...
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .RequireClaim("scope", "GroupManagement")
                .Build();

            options.Filters.Add(new AuthorizeFilter(policy));
        });
        
        // ...
        mvcBuilder.AddAuthorization();
        return services;
    }
    // ...
}

We saw similar code in episode 019, but for a quick overview, we’re building an authorization filter/policy to add to MVC’s filters. Building the policy we indicate what we require, which is the user to be authenticated and have a claim named scope with the value GroupManagement. Given that in the JWT validation we’re ensuring GroupManagement is a target audience, this scope check could probably be skipped in this simple scenario, using it if we want to have multiple scopes in the same API like mentioned previously.

OpenId Connect configuration discovery endpoint

I won’t even try to go into much detail on OpenId Connect discovery, as I don’t really have a complete grasp of the whole protocol, but I think it’s interesting to have some understanding about what’s going on and not just rely on the “magic” that happens with 4 or 5 lines of code.

As I mentioned previously, providing the auth service url to the JWT validation services goes a long way because OpenId Connect specifies ways of using well known endpoints to gather required information.

If we grab the auth service url and append to it /.well-known/openid-configuration we get a JSON response with the aforementioned information.

https://AUTH-SERVICE/well-known/openid-configuration



{
  "issuer": "http:\/\/localhost:5005",
  "jwks_uri": "http:\/\/localhost:5005\/.well-known\/openid-configuration\/jwks",
  "authorization_endpoint": "http:\/\/localhost:5005\/connect\/authorize",
  "token_endpoint": "http:\/\/localhost:5005\/connect\/token",
  //...
  "frontchannel_logout_supported": true,
  //...
  "scopes_supported": [
    "openid",
    "profile",
    "GroupManagement",
    "offline_access"
  ],
  "claims_supported": [
    "sub",
    "name",
    //...
  ],
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    //...
  ],
  "response_types_supported": [
    "code",
    "token",
    //...
  ],
  "response_modes_supported": [
    "form_post",
    "query",
    "fragment"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "code_challenge_methods_supported": [
    "plain",
    "S256"
  ]
}

I cut off a bit of content to avoid a massive wall of JSON, but wanted to point out some interesting bits:

  • the endpoints the provider exposes are indicated here
  • the jwks_uri endpoint provides a set of public keys that the applications can use to validate the JWTs created by the auth service, signed by its private key
  • besides endpoints, there are indications of capabilities supported, like frontchannel_logout_supported, the scopes and claims supported, etc

Sample JWT

Since we’re checking out some “behind the scenes” stuff that we could ignore, as it’s mostly handled for us, but it’s always good we know what’s going on in our applications, we can take a look at a sample JWT.

Remember the JWT is not encrypted, just signed so any tampering is detected, so we can grab it and check out what’s in there. If we grab a JWT and put it in jwt.io, we can see its data.

{
  "nbf": 1560702568,
  "exp": 1560702628,
  "iss": "http://localhost:5005",
  "aud": [
    "http://localhost:5005/resources",
    "GroupManagement"
  ],
  "client_id": "WebFrontend",
  "sub": "bbfbc630-6209-43e9-abeb-d26b92d7c76a",
  "auth_time": 1560702553,
  "idp": "local",
  "scope": [
    "openid",
    "profile",
    "GroupManagement",
    "offline_access"
  ],
  "amr": [
    "pwd"
  ]
}

Won’t go through the whole thing, but we can take a look at some familiar things:

  • with client_id we can see that the token was emitted for usage by the WebFrontend client
  • we can see a familiar audience in the aud property, GroupManagement
  • in the scope array we see the scopes we discussed in the previous post

Outro

That should do it of integration of JWT based authentication in the group management API. As we saw, being IdentityServer the provider or something else is irrelevant from the point of view of this API, as long as it provides the required features.

Links in the post:

The source code for this sub-series of posts is scattered across a bunch of repositories in the “Coding Militia: ASP.NET Core - From 0 to overkill” organization, tagged as episode021.

Sharing and feedback always appreciated!

Thanks for stopping by, cyaz!