In this post, I am going to demonstrate how JSON Web Tokens (JWTs) can be used in conjunction with ASP.NET Core’s new policy-based authorisation model to easily control user access to your Web API controllers and methods.
Note: this is a very long tutorial so if you are comfortable working through source code and figuring things out for yourself (perhaps with reference to the relevant sections below), then you can simply head over to the demo solution
What to expect:
- Configuring Authorisation Policies – this section focuses on the application “scaffolding” such as the middleware requirements and the startup configuration that is used in the example application.
- Issuing JWTs with User Claims – build an account controller that issues the most basic of JWTs with a number of different permutations of user claim types thrown into the mix.
- Restricting Access – build a simple resource controller and restrict access by utilising the policies set up in (1)
- Demo – confirm that it all works!
- An installation of the ASP.NET Core RTM (v 1.0.0) for your platform (I am on Windows 10 using Visual Studio 2015 with Update 3 and ASP.NET Core Tools Preview 2).
- The source code for this demo so that you may see where the individual pieces I refer to in the gists below fit into the whole.
- That you know what JSON Web Tokens are
- That you know how to create a Web API project with ASP.NET Core MVC
- That you know where the ASP.NET documentation is for anything not specifically mentioned that might be new to you (also feel free to ask in the comments)
Configuring Authorisation Policies
OK, so you’ve generated a Web API solution and now it is time to get the authorisation policies configured. Heading over to Startup.cs, the first thing I always do is to lock down everything. My preference is to enforce application-wide authentication by default so that public access must be specified explicitly through the application of the AllowAnonymous attribute. This way we can rest assured that no authentication holes are created by accident and it is easily done in the ConfigureServices method as follows:
Next comes the configuration of the authorisation policies themselves, which is also done in ConfigureServices
Three different policies are created and added to the application’s services:
- Lines 8 – 12: create and configure an authorisation policy named “OnlyValidUsers” with the only requirement being that the user must be in the “ValidUsers” role.
- Lines 14 – 19: create and configure an authorisation policy named “ValidUserWithSeriousClaims” with the requirements that the user must have the username “Valid User With Serious Claims” and also have the country type claim of “This is SPARTA!”
- Lines 21 – 25: create and configure an authorisation policy named “ValidUserWithoutAnySeriousClaims” with the only requirement that the username must be “Valid User Without Any Serious Claims”
And that is it for the authorisation policy configuration, next up, a (very) simple JWT authentication server.
Issuing JWTs with User Claims
Note: I am not going to go into detail regarding the issuing of JWTs in this tutorial, if you would like to learn more, see my previous posts on “Issuing and authenticating JWT tokens in ASP.NET Core WebAPI” which is broken into parts I and II
In order to test the authorisation policies created in the previous section, we need some way to serve up the various user tokens that satisfy these policies (or don’t). For this, I created an AccountController that will issue our JWT’s. No additional packages need to be installed, but you do require a “using” import for System.IdentityModel.Tokens.Jwt that contains the JwtSecurityToken used in this controller. The line by line discussion follows below as usual.
- Line 1: set up the default routing for the controller
- Lines 4 – 12: set up the JWT signing credentials (the logic here should ideally belong to a separate service and injected into this controller, but in order to keep everything easily accessible and visible, I decided to do it in the AccountController itself)
- Lines 14 – 60: these three methods return the JSON responses that contain the JWTs for the three types of users I am using in this example. Important to note is the explicit use of the AllowAnonymous attribute (lines 15, 30 and 42) that makes these methods accessible to unauthenticated users (i.e. public consumption). Also note that all three users have the exact same basic claims added to their tokens (even the invalid user, see lines 19, 34 and 49). The notable differences are therefore added through
- Line 22: adding the serious claim to the “Valid User With Serious Claims” and
- Lines 56 – 57: signing the invalid user token with different signing credentials than that used for the two valid users.
- Lines 62 – 72: adds the basic claim structure to all three users
- Line 66: populate the standard JWT “sub” (subject) claim with the username
- Line 67: populate the JWT “jti” (JWT token ID) claim with a uniquely generated GUID (see line 98)
- Line 68: add the “ValidUsers” role to the user claims (it is important to remember this, all three users in the example have this role)
- Line 69: add the name claim
- Lines 74 – 94: generate a JSON response containing the JWT for the identity provided (I set up the most basic of JWTs). Note that I only override signing credentials for the invalid user and that the JWTs have an expiry of 10 min from time of issue.
Chew on this for a bit and make sure you understand what is happening here, the most important take-aways in terms of how we are going to prove that the authorisation policies work are that (1) all three users have the “ValidUsers” role, (2) only one has the serious claim (country) and (3) only the invalid user’s token is signed with invalid signing credentials to mimic an unauthenticated user.
So far, I have configured the required authorisation policies and created a basic JWT authentication server. I now have to add the JWT bearer authentication middleware so that ASP can take care of the auth on incoming requests. Fortunately for us, Microsoft has been kind enough to provide JWT bearer authentication middleware for ASP.NET Core in the form of the Microsoft.AspNetCore.Authentication.JwtBearer package (add as a dependency in your project.json file) that I configured as follows:
Note: This is not a comprehensive example of how the JWT bearer authentication middleware can be configured. I did the bare minimum to get it working for this demo as the focus here is on authorisation policies.
The token validation parameters are pretty self-explanatory:
- Lines 11 – 12: require a valid issuer (who issued the token?)
- Lines 14 – 15: require a valid audience (who is requesting information?)
- Lines 17 – 18: require a valid signing key (this is how we are going to catch the Invalid User in the demo)
- Lines 22 – 27: add the JWT bearer authentication middleware to the application middleware pipeline (remember that the order in which middleware is added is important, so you would generally add any auth-related middleware first).
Finally, I built a basic “resource server” (just a simple controller in the example solution) that I apply the authorisation policies to for testing purposes.
- Line 1: we apply the “OnlyValidUsers” policy to the entire controller (remember that the “OnlyValidUsers” policy’s only requirement is that a user must have the “ValidUsers” role, refer to Startup.cs)
- Line 5: restrict access to the “ValidUserWithSeriousClaimsData” method to the “ValidUserWithSeriousClaims” policy (the policy requirement is a specific username and the country claim, refer to Startup.cs)
- Line 13: by now it should be obvious
- Lines 21 – 27: notice that there are no specific policies applied to the “InvalidUserData” method. However, the member is still subject to the “OnlyValidUsers” policy applied at the controller level.
What is very important to note is that authorisation policies are applied cumulatively. In other words, with reference to the ResourceController above, in order for a request to reach the “ValidUserWithoutAnySeriousClaimsData” method, it will have to satisfy both the “OnlyValidUsers” AND the “ValidUserWithoutAnySeriousClaims” policies.
I will prove this behaviour to you in the Demo section that is up next.
The long and short of it is that I have a number of buttons, some requesting the JWTs for the three users built up in the AccountController and some requesting data from the ResourceController using these JWTs as credentials. If the authentication request (the call to the AccountController) is successful, the user’s JWT is stored in local storage (in the browser), when the request is made to the ResourceController, the JWT is retrieved from local storage and added to the request’s authorisation header.
- Lines 4 – 11: set up the two buttons to test the “Valid User With Serious Claims” access
- Line 6: this button logs the user in
- Line 10: this button requests the user’s data
- Lines 23 – 38: “log in” as the “Valid User With Serious Claims”
- Line 24: create an XMLHttpRequest object
- Lines 25 – 35: configure the request so that, upon success, the JSON response is added to the “response” element in the HTML (line 16, for convenience so that we may easily view it) and so that the JWT is stored in the browser’s local storage under the ‘jwt’ key (line 33). Note that we expect the ‘access_token’ property on the JSON response object since we generate the JSON that way in the “TokenResponse” method on the AccountController.
- Line 36 – 37: open the POST to the “ValidUserWithSeriousClaimsLogin” method on the AccountController and send it (the localhost and port number posted to is configured in launchSettings.json and is NOT general, but specific to the sample application).
- Lines 40 – 61: request the user data for the “Valid User With Serious Claims” using the user’s JWT for authorisation
- Line 41: create an XMLHttpRequest object
- Lines 42 – 54: configure the request so that, upon success, the JSON response text is added to the “response” element in the HTML and, on failure, the error message and status is displayed instead.
- Line 55: open the GET to the “ValidUserWithSeriousClaimsData” method on the ResourceController
- Line 59 – 60: set the ‘Authorization’ (note the American spelling!) request header with the user’s JWT (retrieved from the browser’s local storage) as ‘Bearer’ (note the space between ‘Bearer’ and the token) and send it.
Running the demo application will launch the following page in your browser (without the responses):
- Button 1: “logs in” and authenticates the “Valid User With Serious Claims”
- Response 1: the response received for “Valid User With Serious Claims” containing the JWT token
- Button 2: requests the “Valid User With Serious Claims” data using the JWT retrieved in the previous request
- Response 2: the response received for “Valid User With Serious Claims” containing user data
- Button 3 + 4: same as 1 + 2, but for the “Valid User Without Serious Claims”
- Response 3 + 4: same as 1 + 2, but for the “Valid User Without Serious Claims”
- Button 5: this is where things become a bit more interesting, this button uses the JWT for the “Valid User Without Serious Claims” (retrieved with button 3) in a data request for “Valid User With Serious Claims”
- Response 5: since “Valid User Without Serious Claims” does not have the country claim and therefore does not satisfy the “ValidUserWithSeriousClaims” policy, the authorisation middleware prevents the user from accessing “ValidUserWithSeriousClaimsData” in the ResourceController and returns a 403 (Forbidden)
- Button 6: now, normally, your system obviously wouldn’t generate tokens for unauthenticated users (the very fact that they’re unauthenticated means they aren’t in your system), but we need to prove that the middleware prevents unauthenticated users from accessing your data. This button therefore requests the user token that we signed with the invalid credentials in the AccountController for our invalid user.
- Response 6: the invalid user response containing the JWT
- Button 7: attempt to access the “InvalidUserData” method in the ResourceController (remember that, even though there is no authorisation attribute applied directly to the method, the entire controller is subject to the “OnlyValidUsers” policy. More importantly, also remember that the invalid user token actually contains all the claims necessary to satisfy this policy’s requirements)
- Response 7: even though the invalid user token contains all the necessary claims to satisfy the “OnlyValidUsers” policy, the JWT authentication middleware is expecting the signing credentials I configured it with in Startup.cs and therefore blocks the invalid user request, returning a 401 (Unauthorised)
Play around with this (just remember that the local storage JWT gets overwritten with every user “log in” so if you click button 1, followed by button 4, you won’t get response 4) and also have a look at the actual HTTP requests and responses (this is easily done with, e.g. Chrome’s developer tools).
There is a lot going on here, you need to keep track of what you configured in Startup.cs, reference the users authenticated (or not) in AccountController and keep track of which authorisation policies are applied to which sections of the ResourceController.
If anything is unclear or doesn’t work as you expect or you would like additional information in any particular section, please feel free to ask or mention it in the comments.
(If you got this far and liked what you read, consider following me @thewillhallatt if you would like to be notified of future articles)