I asume you’ve got at least a basic familiarity with terms such as:
- (web) API
- identity provider
- App startup in ASP.NET Core (how the
Startup.csfile 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#:
JwtBearerDefaults.AuthenticationScheme is equal to
"Bearer". The bits that we’re interested in are:
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
- somehow modify these options in place
- return a
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
… 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:
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 with
api1scope 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
The project with the runnable code can be found here.