Securing an API written in Saturn Framework with Client Credentials Flow and IdentityServer4
If you’re trying to figure out how to secure your API written in Saturn Framework with Client Credentials Flow - this post is for you.
I asume you’ve got at least a basic familiarity with terms such as:
- (web) API
- oAuth
- identity provider
- App startup in ASP.NET Core (how the
Startup.cs
file usually looks like and what is it used for) - Saturn Framework itself (you can read more about its philosophy here)
You’ve also probably digged through the IdentityServer4 quickstart tutorial which this post is based on.
Client Credentials oAuth flow
Say you want to create a web API which will be called by some other application running without any input from the end-user (perhaps your backend services calling one another).
Taking a look at the Client Credentials Flow diagram - it’s the case of one service (client) calling the other (API), but first authenticating with the identity provider of your choice (IdentityServer in this case):
JWT Authentication setup
Let’s first look at how the configuration of the API would look like if we’d written it in C#:
… where JwtBearerDefaults.AuthenticationScheme
is equal to "Bearer"
. The bits that we’re interested in are:
and
We want to replicate the above while using Saturn’s application Computation Expression. Luckily it does have a custom operation called use_jwt_authentication_with_config
which we can use to configure the application’s IServiceCollection services
and IApplicationBuilder app
as shown above. Let’s look at the (current as of 04.10.2020) implementation of use_jwt_authentication_with_config
operation to better understand it:
Application Computation Expression works like a builder pattern, where you’re modifying its internal state: ApplicationState
until you’re ready to build and actually use the app. The function above adds the UseAuthentication()
middleware and adds the authentication service to the DI container (AddAuthentication(...)
). How would we actually invoke this operation so that we can enable JWT authentication?
use_jwt_authentication_with_config
expects us to provide a function of type JwtBearerOptions -> unit
. Looking at it, we can conclude that this function has to:
- take already existing
JwtBearerOptions
- somehow modify these options in place
- return a
unit
Here’s an example of how you’d use this operation:
Authorization at the API
We also need to specify which endpoints require authorization for being accessed. In C#, as the original tutorial says, we should first define the policy in the ConfigureServices
method in API’s Startup
class:
… so that we can then enforce this policy at various levels, for instance at the controller level by using the AuthorizeAttribute:
The first bit (adding Authorization Policy to services) we can solve by using the use_policy
operation. Unfortunately (or perhaps I just couldn’t find an option) Saturn doesn’t let you use the AuthorizationPolicyBuilder as shown above out of the box, so you have to work around it and inspect the AuthorizationHandlerContext directly as described in Policy-based authorization in ASP.NET Core:
When it comes to enforcing the policy, we can use Giraffe’s Authentication and Authorization functions, in this specific case - authorizeByPolicyName
that we plug in into the router’s pipeline. The content of an example HelloController.fs
could be:
Then in Router.fs
we could route incoming requests to our Hello.Controller.helloRouter
router function:
This way we achieve the following:
/api/hello/
resource can be accessed by anyone/api/hello/*
resources can be accessed only by authenticated clients withapi1
scope in their JWT token.
You’d obviously also need to configure the IdentityServer by defining a client that can access this API. For explanation I’m again redirecting you to the IdentityServer’s official quickstart tutorial mentioned above.
Testing the API
We can then check that the authorization works by running the API and IdentityServer instances and invoking some cURL requests:
# Send a request to the public, unprotected endpoint:
curl --request GET --url 'https://localhost:8085/api/hello/'
# Send a request to the protected endpoint without attaching the access token (in Authorization header).
# You should get 401 Unauthorized:
curl --request GET --url 'https://localhost:8085/api/hello/test'
# Retrieve an access token and store it into a variable:
$access_token = curl --request POST --url 'https://localhost:5001/connect/token' `
--header 'content-type: application/x-www-form-urlencoded' `
--data 'grant_type=client_credentials&client_id=client&client_secret=secret&scope=api1' `
| jq .access_token
# Send a request to the protected endpoint with the access token attached:
curl --request GET --url 'https://localhost:8085/api/hello/test' `
--header "Authorization: Bearer $access_token"
Excercise for the reader: add a new scope (say api2
) in IdentityServer configuration, allow the client to retrieve tokens against it, and try getting a token for this new scope only. Then - call the API with it. You should receive 401 Unauthorized
response, as the API allows only tokens with api1
scope!
The project with the runnable code can be found here.