Now that’s a long title!
You probably know .Net core and you probably know Identity Server.
I hope you know REST too.
That is not what this post is about.
What this post is about, is how to setup Identity Server to generate JWT tokens for our REST API calls to be secured.
Furthermore, how to do this in .Net Core.
Yeah, I will need that long title…
Setup Identity Server
I won’t go into details on how to setup IS4.
There is already a lot on information on the web about it including the official documentation.
What we need here is to access IS4 Admin panel, and create:
- a new client (let’s say with ID
clientid
) – use the defaults
- setup a secret on the client’s configuration (let’s say secret
nooneknows
)
- setup a scope on the client’s configuration (let’s say scope
insidelayer
)
- make sure the
AccessTokenType
is JWT and set a sensible AccessTokenLifetime
(defaults to 3600 but I like 86400 for development)
Setup REST API
Again, I will not explain how to create a .NET Core REST API.
The web is full of information about it, including example source code from microsoft itself.
The purpose here is to help someone integrate authentication into an existing code base, so I think skipping this part is fair game.
How Authentication will work
So, we have our Rest API and we can use Postman or equivalent, to call some dummy controller on it.
Cool.
What we will need is to tell the API server to expect a JWT token on all HTTP requests, more preciselly on the authorization
header.
Then, it needs to validate the token against the issuer of that token (Identity Server in this example).
If the token validates, we allow the request to hit the controller code, otherwise its blocked, returning HTTP 401 Unauthorized
Status code.
Nice.
Using it / Testing
I think it gives the reader a better view of what we aim to achive setting this section before the actual code.
However, you’re free to read this page in any order you like.
First we need to get a token from IS4.
We do that with the following HTTP call to IS4 /connect/token
resource:
# from this it should be trivial to build the postman call
# dont use spaces, they are just there for readability
curl -X POST
-H "Content-Type: application/x-www-form-urlencoded"
-H "Cache-Control: no-cache"
-d 'client_id=clientid &
scope=insidelayer &
client_secret=nooneknows &
grant_type=client_credentials'
"http://identity-server-url:8001/connect/token"
Note that for this to work the grant_type
must be client_credentials
.
You can read the full story here: Grant Types.
It took me some time to find that out.
The response will be something simple where you can extract the token and use it to call our API:
# from this it should be trivial to build the postman call
# dont use spaces, they are just there for readability
curl -X Get
-H "Authorization: Bearer eyJh...the token is really long..."
-H "Cache-Control: no-cache"
"http://our-api-url/api/values"
Here, I am calling the ValuesController
with a GET
request, providing a JWT token.
The server will validate it, accept it, execute the controller code and provide me with a HTTP 200
Status OK response with the data I (don’t) need.
Any other case such as no token provided, the token does not validate, the token format is wrong, the token is expired and so on, the server will reply with HTTP 401
Unauthorized and no data.
The controller code never gets hit.
Seems simple?
It should be.
To use, not to setup I mean…
From theory to code
So, we do all this by decorating all our contollers (all those we want to protect, at least) with the [Authorise]
attribute.
Like so:
[Route("api/[controller]")]
[Authorize]
public class ValuesController : Controller {
// (...) controller code
}
The [Authorise]
attribute can be used at the class level (applies to all public methods) or at the method level for finner control.
There is also the [AllowAnonymus]
attribute that does what the name says.
I find that usefull to allow login
method calls or if your API will allow some type of public/anonymus read only operations.
Anyway, the internet explains all that much better, I just want to leave a remark about it.
Then, we need to tell the code how and where the “token issuer” is.
We do that as follows:
using System.IdentityModel.Tokens.Jwt;
public class Startup
{
// (...)
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
SetupAuthentication(app);
// (...) rest of the code
}
private void SetupAuthentication(IApplicationBuilder app)
{
var auth = new AuthenticationConfiguration();
Configuration.GetSection("authentication").Bind(auth);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
RequireHttpsMetadata = auth.RequireHttps,
Authority = auth.IdentityServerBaseUrl,
AllowedScopes = auth.AllowedScopes,
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
}
}
We can see that we load configurations from the appsettings.config
file, which you can read more about it here.
{
"authentication": {
"IdentityServerBaseUrl": "http://identity-server-url:8001",
"AllowedScopes": [ "insidelayer" ],
"RequireHttps": false
}
}
All the magic happens in the framework method UseIdentityServerAuthentication
.
We just need to add some depencies for the code to compile:
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0",
"type": "platform"
},
"IdentityServer4.AccessTokenValidation": "1.0.2",
"Microsoft.AspNetCore.Authorization": "1.1.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0"
}
}
Not just one dependency but 3 packages to our project.json
file.
That is it.
You can now build and run your API and call it with a valid/invalid token to see that you can/can’t get access to it.
Make it work on development: Policies
All of this is fine.
However, when we are developing the API code we do not want to worry about generating tokens and tokens expiring and so on.
I needed a simple way to disable authentication for the development enviroment.
I could comment out code but that brings more trouble to the table than it solves because now I have to remember not to commit this commented code and which code to comment out if I stop to work on this for a few days (read hours).
Sharing this “commented code” knowledge among several team members is also, hum, stupid.
If this was not .NET core we could inherite from the Authorize
attribute class and role our own with an IF inside.
However, .NET Core developers do not like that messing around and do not allow it anymore.
What they allow is for you to create your own Policy
and use that instead.
Once I knew this, it was peanuts to get the code working.
Finding this out was not so easy.
Google floods me with pre-dot NET core knowledge, and it took some digging.
So, we create our policy according to the environment, like so:
public class Startup
{
private readonly IHostingEnvironment _env;
public Startup(IHostingEnvironment env)
{
// ...
_env = env;
}
public void ConfigureServices(IServiceCollection services)
{
BypassAuthenticationForDevelopment(services);
// ...
}
private void BypassAuthenticationForDevelopment(IServiceCollection services)
{
if (_env.IsDevelopment())
{
services.AddAuthorization(options => options.AddPolicy("AllowDevCalls", policy => policy.RequireAssertion(c => true)));
}
else
{
services.AddAuthorization(options => options.AddPolicy("AllowDevCalls", policy => policy.RequireAuthenticatedUser()));
}
}
}
And we update all of our Authorize
attributes to use the following policy:
[Authorize(Policy="AllowDevCalls")]
The above works with some dependency injection magic that the .Net framework provides us.
I encapsulated some code in a private method for readability purposes only.
Notes on security
You may notice that I am using HTTP to reach Identity Server and setting RequireHttps
to false
which are both obvious mistakes.
I use HTTP to have a simpler development environment and as an example, but all this must use HTTPS to be taken seriously.
Additionally, setting the AccessTokenLifettime
to big values (as I did) is not a good practice.
Setting these times is a somewhat tricky business as it is always a compromise between security and usability.
Invalidating tokens too often will required IS4 to be queried more often.
If this leads to a user login, then the user experience is basically crap.
There are some options like refresh tokens and such, but again, this is a topic on itself and one that is heavily dependent on the problem at hand.
Sources
I.E.: Not all of this comes from my own ideas:
Glossary
Term |
Description |
IS4 |
Identity Server v4 |
DI |
Dependency Injection |
JWT |
JSON Web Token |