In this episode, we look at the backend for frontend, and the changes required for it to handle the users authentication, redirection to the identity provider (the IdentityServer4 powered auth service), the inclusion of an access token when making API calls, the refresh of said token and handling CSRF tokens.
For the walk-through you can check out the next video, but if you prefer a quick read, skip to the written synthesis.
The playlist for the whole series is here.
In the past couple of episodes, we saw how to integrate IdentityServer4 into our auth service, then prepared the group management API to make use of the access tokens (particularly, JWT) it gets on each request to authenticate and authorize the user.
In this episode, we’ll start adapting the frontend, namely its back for front component, to delegate authentication to the auth service, then include the user’s access token in each request to the group management API.
Configure the BFF to authenticate/authorize the user
We’ve seen some ways to implement authentication in ASP.NET Core, and we’re going to see yet another one with the OpenID Connect integration.
The gist is the same as we’ve seen in the previous post on the group management API - we’ll use
AddAuthentication with some specific OpenID Connect configurations in
Startup.ConfigureServices and call
app.UseAuthentication in the request handling pipeline definition.
AddAuthentication has a bit more configurations this time around, so let’s jump right into it.
Let’s start by looking at the code, then go through it step by step.
The actual call to
AddAuthentication doesn’t to that much, configuring the default authentication scheme and challenge scheme. After this call, on the returned
AuthenticationBuilder instance we configure the schemes to match with what we configured.
Cookies authentication scheme is configured with
AddCookie and even though not a lot is going on, it’s an important bit. We’ll see how this works in more detail in one of the next sections, but setting the
options.EventsType allows us to provide an alternative implementation of the
CookieAuthenticationEvents class. By doing this we can, as the name implies, define what happens at certain moments of the cookie authentication flow. In this case, as we’ll see later, we’re hooking up to an event in order to ensure the tokens we have in our possession (namely access and refresh tokens) are still valid and the user can continue to use the app without re-authenticating.
The challenge scheme, named
oidc, is configured with the call to
AddOpenIdConnect. A lot of this should look familiar, as it matches what we configured in the auth service, when we looked at adding
IdentityServer4 to it (in episode 022).
We start by setting the
SignInScheme, which is responsible for persisting the user info we’ll obtain from the authentication process.
Then we set the
Authority with the auth service endpoint, plus
ResponseType with the same values we set when configuring the client in the auth service.
SaveTokens = true means the tokens will be stored in the cookie. By default it’s
false, to keep the cookie smaller, so in the future we probably should extract it to some server side session, but it works ok for now.
true, means that after the OpenID Connect flow is complete and the tokens retrieved, an additional request is made to the user info endpoint to fetch additional user claims.
We add some extra scopes (besides the default OpenID Connect ones),
offline_access so we can access the group management API and use the refresh token.
Finally, we’re registering on the
OnRedirectToIdentityProvider event. As this application will be used as an API by the SPA and not as a full-on MVC application, it doesn’t make sense to redirect API requests to the auth service when the user isn’t logged in, but rather responding with a 401 (unauthorized). To achieve this, in the event handler we’re providing, all unauthenticated requests that go to an endpoint that’s not
/auth/login, we set the status code to 401 and mark the response has handled, otherwise letting it proceed as usual.
Requiring the user to be authenticated
We already saw in the previous episode how to ensure all requests to an API are authenticated, so this will be exactly the same, but I’ll put it here for reference anyway 🙂.
As we saw previously, we go to our
AddMvc call, and in the configuration lambda we create and add an
AuthorizeFilter with the policies we need, in this case for the user to be authenticated.
As you might have noticed, when we registered on the
OnRedirectToIdentityProvider for the OpenID Connect authentication challenge scheme, we made it respond with a 401 for all requests except for the
/auth/login route. That’s because we’ll use this route to put a login link in the SPA.
In summary, the login strategy will be the following:
- We expose an
/auth/infoendpoint that the SPA can invoke. If it gets a 401, the SPA shows the link for logging in, otherwise it makes use of the returned information.
- We expose an
/auth/loginendpoint, with the sole purpose of redirecting to the auth service and then, when the authentication is completed, redirect to the desired SPA url.
Let’s see the
AuthController’s code, developed to implement the aforementioned login strategy.
/auth/info endpoint simply returns some of the current user’s information (plus some CSRF token logic we’ll see later). It returning 401 on unauthenticated users is handled automatically by the framework, as we saw when we added the authorization filter and transformed the redirect in a 401 in the authentication challenge scheme.
/auth/login endpoint, gets a return url, so it can redirect back the user to the correct place in the SPA. Again, the handling of the unauthenticated users, redirecting to the auth service, is done automatically by the framework.
Support cross-site request forgery token
To add support for cross-site request forgery token, we have a wee bit more work than what we saw in previous episodes with MVC and Razor Pages. That’s because since we’re using a SPA instead of Razor generated HTML, so we got some more manual work to do.
Let’s begin with
Startup.ConfigureServices. In here we need to make a couple of changes: add a filter to validate the presence of an anti-forgery token in all requests that should have it (so, all except
TRACE) and add helper anti-forgery services.
In the lambda that configures MVC, we can see the new filter being added to validate the anti-forgery tokens.
After the filter, we can see the configuration of the anti-forgery services, with the call to the
AddAntiforgery extension method. The
HeaderName property we’re setting, is the name of the header that the SPA will send with the anti-forgery token, so it can be validated server side. If we didn’t set this, the server would only try to find the token in the submitted form data, which is the typical behavior in MVC/Razor Pages applications.
Now for the SPA to be able to set the header, it needs to get it from somewhere. That’s where we get back to the
AuthController. In the
GetInfo action, we’ll generate the anti-forgery token and set a cookie with it, so that the SPA is able to use it.
We now get a dependency in the constructor,
IAntiforgery which allows us to generate the anti-forgery token. We make use of it in the
GetInfo action, where we can see that besides generating the token, this service also stores it so it can validated when the client application sends it back in the header.
For the SPA to get the token, we’re adding the cookie and setting its
HttpOnly property to
false, otherwise the browser wouldn’t allow for the application to access it.
Provide the access token on API calls
Probably the first thing that comes to mind when we think that we must send the access token in each request we’ll make to the API, is going to the place we’re making the requests and add the header. This would certainly work, but we can use a cleaner solution, which is very reusable: create a class inheriting from
DelegatingHandler and configure the
HttpClients we want to use it.
DelegatingHandler allows us to intercept the
HttpClient’s requests, so we can add some logic before, after or even instead of the request. In this case, we want to add the access token header before sending the request. The following class
AccessTokenHttpMessageHandler implements this logic.
Not a lot going on, but let’s go through it quickly. We’re simply overriding the
SendAsync method, where we can implement the logic we require. We have an
IHttpContextAccessor instance injected, that we use in
SendAsync to grab the access token from the
HttpContext (remember we stored it in the cookies, and can fetch it in this fashion).
With the token in hand, we can add it to the request headers, then pass it on to the base implementation of
SendAsync, letting the rest of the request flow as normal.
Now we just need to make a couple of changes in
Startup.ConfigureServices, to use the handler: register it in the dependency injection container and configure it with the desired
As you can see, not a lot to do, we just need to remember to do it 😛. Register the
AccessTokenHttpMessageHandler with the DI container, while also registering the
IHttpContextAccessor, as it’s not there by default in ASP.NET Core. After that, we can make use of the handler, by calling the
AddHttpMessageHandler extension method on the
Note: a better way to register the
IHttpContextAccessoris to call the
AddHttpContextAccessorextension method, but I didn’t know about it when I originally wrote this code 🙂.
Refresh the access token
Now the final thing we need to do to wrap this up, is to make sure we refresh the access token when its expired (or about to).
To handle this, as I briefly mentioned, we have an alternative implementation of
CookieAuthenticationEvents and some extra helper classes. This means that in each request received, we’ll check for the validity of the access token, and if it’s expired we refresh it.
Another possible approach to checking the validity of the access token, would be to do it in the
HttpClient’s delegating handler we implemented. The main difference is, with the cookie authentication events, we make this check once per request to the BFF, while if we do it in the delegating handler, we do it once per request to the backing API. Both have pros and cons, but it seemed like a good opportunity to check out more ways in which we can hook up to the framework’s extensibility points, so we’ll go with this one 😉.
Let’s start with the
CustomCookieAuthenticationEvents class. When inheriting from
CookieAuthenticationEvents, we have a bunch of methods we can override. In this case, we want to override
ValidatePrincipal - if the access token is valid or we are able to refresh it, we can let it flow as normal (with maybe some tweaks), if not, we reject the principal and force a logout, so the user needs to re-authenticate.
There are still a couple of missing pieces, but we’re able to understand what’s going on in the
CustomCookieAuthenticationEvents anyway. We get an instance of
ITokenRefresher in the constructor, which we’ll see in a bit in more detail, but as the name implies, takes care of refreshing (if needed) an access token. If the token refresh fails, we reject the principal and force logout. If the token is refreshed, then we need to update the values that are stored in the cookie and renew the cookie for the user (hence the
context.ShouldRenew = true). Another scenario is that the token might still be valid, so we don’t need to do anything (hence no specific code for that scenario).
Before looking at the
ITokenRefresher and its implementation, let’s see the
TokenRefreshResult class which is returned by
As we can see, besides having the necessary properties to check the results in the
CustomCookieAuthenticationEvents class, we have some helper methods that the
ITokenRefresher implementation can use to achieve nicer readability.
ITokenRefresher interface exposes a single method,
TryRefreshTokenIfRequiredAsync which… does what the name says 😛
Now let’s see its implementation.
The code isn’t super complex, but let’s go though it carefully, as there are some interesting bits in it.
Taking a look at the constructor, 2 of the dependencies injected are well known by now, but the other isn’t.
IDiscoveryCache is a helper provided in the
IdentityModel NuGet package, allowing us to get the info from the discovery endpoint of the OpenID Connect provider (and cache it) so we can use it in other requests to the provider, in this case, for token refresh.
Getting into the
TryRefreshTokenIfRequiredAsync method implementation, we have:
- Check if we have a refresh token, if not, it’s a fail.
- Check if the token expiration date is below a threshold, if it isn’t we can return a
- Fetch the provider information with the
- Make a request to refresh the token, using the info we configured in
Startupclass as well (yes it should come from configuration and not be hardcoded here 🙃). Note that the
RequestRefreshTokenAsyncis an extension method from the
IdentityModelNuGet package, simplifying the creation of the refresh token request.
- If the token refresh was successful, return success with the required information, otherwise return a failure.
And that should do it for the token refresh logic. It’s not a lot, but a good chunk of code nonetheless. Maybe we can put it in a shared package if we need to reuse it in other components of the PlayBall application.
PS: don’t forget to register all the required services in the DI container.
Wrapping up, we’ve seen how to delegate the user authentication to the auth service, while getting and maintaining the tokens to access the backing APIs (right now, only the group management API). We also saw how to get the CSRF token infrastructure ready to work with a SPA, instead of a the usual server side rendering with Razor. In the next episode, we’ll wrap up the sub-series, by seeing the changes made to the frontend to play well with the BFF and integrate everything we developed so far.
As a reminder (and I’ll probably repeat myself in the next episode), all of this is just one of (probably) many possible ways of tackling this problem, so if you have a different idea and think it’s better, go for it 🙂 (and let me know so I can improve mine).
Links in the post:
- Cross-Site Request Forgery (CSRF)
- Episode 023 - Integrating IdentityServer4 - Part 3 - API - ASP.NET Core: From 0 to overkill
The source code for this sub-series of posts is scattered across a bunch of repositories in the “Coding Militia: ASP.NET Core - From 0 to overkill” organization, tagged as
Sharing and feedback always appreciated!
Thanks for stopping by, cyaz!