As part of the recent discussions around how to build clients for OpenID Connect and OAuth 2.0 based systems (see e.g. Brock’s post here), we substantially updated our workshop and supporting libraries. The updated material (both workshop and break-out sessions) will be part of the upcoming conferences (Oslo, London, Porto, Copenhagen…).
One piece of the puzzle is how to manage OAuth 2.0 access tokens in ASP.NET Core server-side (e.g. MVC) web applications (another piece can be found here – more details soon.
In a nutshell, a client for a token-based system has these fundamental jobs:
- Request the token
- Use the token
Easy right? Well – there is one step missing (which is actually the most complicated): manage the token.
Looking a bit closer, the actual tasks are:
- Use OpenID Connect to authenticate a user and start/join a (single sign-on) session. This will result in an identity token that is used to validate the authentication process
- This identity token must be stored somewhere to do a clean sign-out at some later point
- As part of that you also request an access and refresh token to be able to communicate with APIs on behalf of the logged-on user
- These tokens must be also stored somewhere secure so that the application code can use them when doing API calls
- When the access token expires, use the refresh token to request a new access token and make this new token available to application code
- At sign-out time, use the identity token to authenticate the sign-out request, and revoke the tokens that you don’t need anymore (e.g. the refresh token)
- Make it work in a web farm
While you can build all of that from scratch – let’s have a look at what ASP.NET Core has built-in to assist you.
OpenID Connect Authentication Handler
The OIDC support in ASP.NET Core is pretty full featured. It supports Implicit Flow for authentication only scenarios and the more secure Hybrid Flow (aka code id_token) for authentication and access/refresh token requests. It would be nice if it would also support “Code + PKCE” out of the box, but that’s a topic for another post.
Most importantly, the handler has the concept of persisting the tokens into the authentication session. IOW – it makes the OIDC artefacts like identity, access and refresh token available to application or plumbing code.
If the identity token is available in the authentication session, it will be automatically used as a hint at sign-out time to authenticate the sign-out request to the identity provider.
The Authentication Session
ASP.NET Core has the concept of an authentication session. Even if they put that behind a whole lot of abstraction APIs, for all practical purposes, this is technically implemented by the cookie authentication handler – and – well a cookie.
Don’t confuse that with the more general session concept – the authentication session is solely for identity/security related information like claims, tokens, expiration times etc.
If you set the SaveTokens options on the OpenID Connect handler to true, the handler will persist the tokens and corresponding metadata in the session. You can then later access them via the GetTokens extension method on HttpContext, or by directly calling AuthenticateAsync on the name of the sign-in scheme. This returns an AuthenticateResult, which again has a properties dictionary containing the tokens.
Technically speaking the tokens are serialized into the authentication cookie, at least by default. You might not like that, because it potentially increases the size of your cookie substantially. Another good reason to keep those tokens small.
Another option is to plug in a server-side storage mechanism, e.g. Redis. This way all the data is stored server-side, associated with a GUID – and also the GUID is emitted into the cookie. For that you need an ITicketStore implementation and set that on the SessionStore property on the cookie handler options.
The cookie handler also has support for events, e.g. whenever a cookie is received, or when sign-out is happening. This is a convenient place to wire up automatic token management, e.g.
- On every incoming request, check the expiration time of the current access token, and if a certain threshold is reached, use the refresh token to get a new access token
- At sign-out time, call the revocation endpoint at the token service to revoke the refresh token
With that in place you can implement all necessary token management features at the runtime level, and your application code is completely unaware of these details. It simply uses the current access token from the authentication session. If an API call returns a 401, this means that the token management layer was not able to keep the token “fresh” and manual steps (e.g. a new authentication request) is necessary.
Sounds a bit abstract? Find a working sample here…
Some key points about the sample:
- All the cookie events are handled in a dedicated class which gets instantiated using the DI system
- the ValidatePrincipal method is called on every request and contains the refresh logic
- the SigningOut method is called at sign-out time to do the token revocation (for both self-initiated sign-outs as well as sign-out notifications).
- All the OAuth protocol work is done by IdentityModel and is just a matter of calling some extensions methods on an HttpClient retrieved from the HttpClientFactory
- Using some ASP.NET config black magic, you can condense the whole thing to one line of code .AddAutomaticTokenManagement (yay)