Securing a web application written in Saturn Framework with Authorization Code Flow and IdentityServer4
Comparing to the Client Credentials Flow which I described in my previous post - the Authorization Code Flow involves one more entity - the End-User (aka Resource Owner). That makes the whole process “interactive”, since the End-User needs to take an action - log in and allow our application (the Client) to have access to a Protected Resource (for instance - retrieving user’s email, setting, or some user-specific value on behalf of the user, stored behind the API that is protected by the Identity Provider). David Neal explained this flow realy well in his post. I’m assuming you’re familiar with the C# equivalent setup of the IdentityServer4 for Interactive Applications with ASP.NET Core - if not, give it a go, it’s explained really well and should give you a good understanding of how things work.
Just to refresh - here’s the Authorization Code Flow sequence diagram:
Krzysztof Cieślak has explained how to use the GitHub provider in Saturn, but even though the setup is really similar, it still took me a while to make it work with IdentityServer4 properly, I’ve also struggled with logging out which I’ll elaborate on below.
You can follow along by cloning the source code from this repository.
Solution setup
The solution involves two services: the web application itself, and the Identity Provider (IdentityServer4 in this specific case).
For the former - you can either use the template (simpler option):
dotnet new -i Saturn.Template # Run it only once - when you've never used the Saturn template before.
mkdir SaturnWithIdentityServerInteractive && cd SaturnWithIdentityServerInteractive
dotnet new saturn -lang F#
dotnet tool restore
… or set it up manually (if you know what you’re doing). I’ve used the template, but removed all the irrelevant code which would obscure the main topic of this post (error handling, database migration etc).
Setting up IdentityServer4 is really simple, you just need to create the project out of the is4inmem
template (run these commands from the root directory of the project, SaturnWithIdentityServerInteractive
in my case):
cd src
dotnet new -i IdentityServer4.Templates # Run it only once - when you've never used IdentityServer4 templates before.
dotnet new is4inmem -n IdentityServer
cd ..
dotnet sln add .\src\IdentityServer\IdentityServer.csproj
Then in Config.cs
set up the Clients
variable to:
This should give you a ready-to-run instance with two logins: alice
(password: alice
) and bob
(password: bob
) and a set of client credentials (client_id: interactive
, client_secret: secret
). Sweet!
Saturn application settings
To use the OpenID Authentication we need to set up the services to use Authentication, Cookie and OpenIdConnect handlers. In C# we’d do it like this:
Luckily - Saturn.Extensions.Authorization package contains an Application Computation Expression Custom Operation: use_open_id_auth_with_config
which helps us to achieve exactly this. In order to be able to use it - you need to add nuget Saturn.Extensions.Authorization
to paket.dependencies
and Saturn.Extensions.Authorization
to src\SaturnWithIdentityServerInteractive\paket.references
files. Then run dotnet paket install
and dotnet restore
.
Looking at the implementation (as of 13.10.2020) we can see that the code is semantically similar to the C# equivalent mentioned above:
If you’re not sure why exactly do we specify different Schemes in the authConfig
(as asked here) - see this brilliant answer.
We should use the use_open_id_auth_with_config
operation like so:
How can we leverage this protection now? Let’s say our application wants to access a protected secret of the user - their favourite colour. Our Router.fs
would look like this:
We’ve got the defaultView
- the home page which can be accessed by anyone, and the protectedView
(under /protected
). It can be accessed only by authenticated users, and is set up like this (in ProtectedHandlers.fs
):
protectedViewPipeline
uses the Giraffe’s requiresAuthentication
together with challenge
- which uses the OpenIdConnectDefaults.AuthenticationScheme
scheme (being equal to "OpenIdConnect"
as mentioned above). Pay special attention to this value - we don’t want to be using CookieAuthenticationDefaults.AuthenticationScheme
as this is a different scheme which doesn’t handle the OpenIdConnect protocol. It will not redirect the user to the Identity Provider (https://localhost:5001/account/login
), but to the application itself (https://localhost:8085/account/login
) which will result in 404
error if you don’t have the account/login
endpoint. That’s a no-no!
protectedHandler
retrieves user’s details (given_name
) from the token claims (remember to add the profile
scope to OpenIdConnectOptions
in the application setup!), retrieves user’s secret colour by calling the API with the Client Credentials Flow and provides these values to the view which returns a personalised “secret” webpage. If the user doesn’t have the secret colour yet, one is informed about it as well.
Logging the user out
There’s one more bit which took me a while to figure out. Giraffe provides a signOut
handler which signs the user out from a given authentication scheme. The documentation gives an example of how to use it:
Since we want to sign the user out of both "Cookies"
and "OpenIdConnect"
schemes - you’d probably try doing something similar in our Saturn router:
If you log in as bob
, go to /logout
page - you’ll be logged out indeed - but the next time you’d like to log in - the IdentityServer session will still be active - so instead of being taken to the login screen (where now you’d like to log in as alice
) - you’ll be automatically redirected back to your app logged in as bob
. This StackOverflow question describes exactly this scenario. It turns out - we need to use overloaded SignOutAsync
function which accepts AuthenticationParameters
but Giraffe doesn’t offer it out of the box. Hence, we need to write our own wrapper for calling this function (the implementation is based on the signOut
function):
Let’s try it again and it… works! Logging it as bob
, then logging out and logging back in again - we’ll be taken to the login page where we can log in as alice
now.
That should be it! If you’ve got any questions, feel free to post one below or reach out on Twitter.