OAuth Providers and Session Management
With the rise of JWT-based access tokens as a form of authentication for the web, I feel that there has been a weakening emphasis on how logout works and the usefulness of session management.
Log on to your Google account and head into your security settings. You’ll see a list of devices that your account is currently logged in to, or have active sessions with. You can choose to remotely end those sessions, logging those devices out of your account.
Try logging out of Google in your browser, and see if a session replay attack works or not. Logging out removed your session cookie from your browser, but if that cookie were retrieved would it still work?
Now, try doing this on your friend’s quick and dirty website they setup using Auth0, Supabase, Clerk, etc. You’ll find the experience is…lacking. When you rely on an IAM service that only gives you OAuth support, you’re missing out on a lot of other functionality. Functionality that OWASP directly discusses as requirements for a secure web1.
This article discusses my ramblings and musings on OAuth purposes and ue cases, SPAs vs MPAs, cookie management, proper form, and more.
Also, because all of this is random musings, take none of this at face value. This is not proofread or refined from its initial draft and is not a comprehensive or entirely accurate guide. This is me collecting my thoughts.
Cookie Basics
Skip this section if you’re familiar with the basics of cookies and authentication.
Essentially,
- It’s not user friendly to ask for a username and password for each and every page load to a web server that requires authentication.
- So we make the user login once and store that information in a cookie which is automatically sent with each subsequent request, until that cookie times out or the user logs out.
- Authentication cookies should not be tamperable by the user, lest they masquerade as a different user or give themselves extra permissions that they should not be entitled to.
- These cookies should be set as HTTP-only, so that XSS attacks can not make requests to your web server maliciously.
- The auth cookie is not simply a copy of your username and password, so that in the event of snooping (HTTP sniffing or MitM) or leakage (physical computer access, or misconfigured server logging) a cookie can be rendered useless. Plus, people tend to use the same password in multiple areas, opening the user up to attack on websites outside of your control.
There’s many more basics that I didn’t touch on. See OWASP and other guides for a more whole list. This gives us a good starting point, though.
OAuth Basics
Similarly, skip this section if you’re familiar wit hthe basics of OAuth, OIDC, and JWTs.
Essentially,
- A JWT is a plaintext chunk of JSON data, that is cryptographically signed such that it becomes immutable.
- OAuth is a form of granting expiring access tokens from a set of credentials, typically for a user within a platform, in order to allow a user to grant access from one service to another within their identity/permissions.
- Many OAuth providers tend to use JWT-based access tokens. This allows for verification of user authentication locally, without a roundtrip to the authentication provider.
- Current recommendations are to use short-lived (e.g. ~1 hour) access tokens in conjunction with longer-lived refresh tokens.
- Refresh tokens allow for re-retrieval of access tokens without requiring the user to re-authenticate.
- Refresh tokens tend to be opaque, and revokable.
- Access tokens tend not to be revokable.
Authentication is Hard
Implementing a proper authentication system is hard, especially in this day and age. Gone are the days of just usernames and passwords. You’re expected to support OAuth with Google or Facebook. You have to verify that email addresses are valid and owned by the user. You have to support 2FA/MFA, via SMS or TOTP tokens or security keys or passwordless emails or the new passkeys. You have to support logout. You should support session management. You have to ensure strong passwords, and check against databases of leaked passwords. You’re expected to perform device fingerprinting, and alert on suspicious activity. TV apps should have a QR code or a token to input to authenticate. And all the while it has to be done correctly and securely!
Not many people or companies want to spend the time to do this (correctly and securely) when they could be working on their business logic instead. Honestly, that’s totally understandable. If you’re making a small quick-and-dirty website, choosing an authentication provider like Auth0 or others is a no-brainer. I wouldn’t want to code something incorrectly and be on the hook for when I leak user passwords by mistake, after all.
But it’s still difficult to integrate with many of these providers. You have to do your due diligence to provide the right interfacing for the user, and implement all of the correct flows, so as not to miss out on core, security-related functionality.
One of these areas that is forgotten is session management and secure logout.
Why is Logout Important?
I think that the most salient and relatable example of session management gone wrong is the recenty LinusTechTips hack2.
An LTT employee’s Google/YouTube account was infiltrated by hackers, whereby they phished the employee and installed malware on their computer, resulting in the retrieval of browser session cookies. The hackers used these cookies to act as the employee and make changes to the company’s various YouTube channels. Thankfully, LTT was able to mitigate the attack due in part to logging out that employee’s YouTube session, so that the hackers lost their only access into the channels' platforms.
This may be slightly incorrect and missing some other key details, but the idea remains the same: despite HTTPS and HTTP-only, encrypted cookies and possibly even a user logging out of their account, a targeted attack on browser cookies can still provide access to an account. Cookies are so so so important to keep safe! But despite that happening, a session management system that is implemented properly can help mitigate the problem.
An attacked user who only lost a session cookie but not their password can log in and end the session that the hackers are using, rendering the stolen session cookies useless. A stolen cookie should not allow an attacker to change that user’s password without knowledge of the previous password (aside from password reset flows, which also requires access to the email inbox of the user). And to mitigage any further damage, ending a remote session should have an immediate effect.
It’s easy to ignore these problems because they tend not to happen very often. But when an attack does happen, it’s paramount to proper and swift recovery.
Scope
I will be framing the rest of this article in the context of a web site with the following characteristics. Except when I’m not and talking about a SPA instead.
The web site is a Multi-Page Application (MPA). That is, it runs on a server which has many endpoints that return HTML pages. Assume the amount of Javascript is light.
The web site allows for a user to log in and log out.
The website is load balanced, with many servers being able to respond to requests. Let’s assume this is complicated and spans across different data centers.
Possibly, this website is split out into multiple websites under subdomains and deployments of multiple web application services under different subdomains. The authentication cookie should work across these different services.
Possibly, this website has internal backend services, and maybe we want to be able to pass along authentication cookies to them so they can act on the user and their permissions accordingly.
We will keep in mind the latency on verification of authentication session cookies on each request to our web site(s).
We will keep in mind the immediacy of cookie revocation upon logout or remote session ending.
We will keep in mind privileged security sessions for sensitive actions on a user account.
Centralized Session Management Mechanism
The first, most basic, and as some would call it traditional, mechanism I want to talk about for managing sessions and cookies is actually pretty straightforward. Your cookie is an identifier into a session store. The identifier is encrypted for some added security and prevention of guessing other session identifiers. Your web site queries your session data store with the decrypted cookie identifier to yield information about the user’s session before proceeding.
This works! It has worked for a long time, and probably will work for a long time as well. A user can track their devices/sessions because you can simply query your data store for all sessions for that user’s identifier. A user can end a remote session by deleting (or marking deleted or expired) that session in the data store. Similarly, when a user logs out of their current session, the session is deleted or marked expired. This has an immediate effect because any subsequent request to your web site with a replayed cookie will fail. While the identifier in the cookie may be decrypted by the local server node, the identifier will yield an expired or deleted session object, and can reject the request. The centralized session data store provides strong support for session management.
Now, you do have to make sure this scales. After all, we have a lot of different servers in different regions with a lot of traffic. If every (authenticated) request has to check the data store for a valid session, you want this data store to scale up well-enough with the rest of the infrastructure. A SQL database table works, but mainly if your database scales properly and if it’s indexed and maintained properly. A Redis cluster might be a little easier, since you can add more nodes to a cluster and they’ll deal with moving data around. It seems to be well-optimized for ready-heavy workloads, so that your fleet of infrastructure can query the read-only nodes quickly and evenly. I recall Redis may partition data to different cluster nodes but I think that requests to the nodes go to the correct node ahead of time, or something like that. Otherwise, there’s other great solutions to this.
Simply put, a round trip to a centralized store isn’t so bad, when you have full control of it.
Ory Kratos
Ory Kratos is an IAM service/application that works much like this, though with an HTTP API layer. You implement your own UI which utilizes the Kratos APIs for login, logout, session management, MFA, etc.
On login, Kratos mints a cookie value for you to set for the user’s browser. You set that cookie, and subsequent reqests include that cookie. Now you have to check that the cookie is valid. To do so, you hit an HTTP API on Kratos, passing the cookie through and retrieving back session information (if any).
Now, this does raise some questions. Chief among them being: can Kratos scale and not become a bottleneck for our flurry of authenticated requests?
Kratos is doing all the hard work of authentication for us. But it’s still a dependency, with an API layer you work with and integrate to. We would like to be able to utilize this instead of rolling out our own auth with a custom distributed Redis-backed session store like in the example above. We can keep Kratos up to date and not worry about security, but we may worry about performance if we can’t control Kratos directly.
I haven’t looked into how Kratos stores its session information, or the details of its Docker/manual deployment and how it can scale with some fine tuning. It’s entirely possible (and maybe likely) that it can scale well for your needs. Deploy Kratos to a Kubernetes cluster with the right configurations and let it serve you well!
There is also the required mention of Kratos' cloud/service offerings, where you interface with Kratos over the public internet instead of within your own infrastructure. That’s gotta be slower, right? Well yes, obviously; but they’ve obviously understood that session checking latency can be disruptive and worked on edge deployment to minimize latency as much as possible. I haven’t used it, but I believe that they’ve done their due diligence in making it performant.
Eliminating the Roundtrip
And yet, you still might not like the idea of having a roundtrip for this session checking step in your authentication pipeline for your infrastructure. Having all of your infrastructure be beholden to this service, while meeting other performance goals, could for some reason or another be an issue.
Forms Authentication and Encrypted Cookies
If you’ve had experience with some frameworks like Microsoft OWIN, you might be familiar with encrypted the encrypted cookie approach to solving this problem. Here, the cookie is not an identifier to a session store object, but rather an encrypted blob that contains all of the necessary information to show that a user is authenticated. The only stipulation is that each server or node in your infrastructure needs to have access to the same secret key(s) to decrypt the cookies appropriately. If there is successful decryption, then the server knows it can trust the cookie’s payload values, which may include a user identifier, authorization scopes, display name, and more.
These cookies tend to include expiration timestamps as well. Not only on the HTTP cookie itself, so that the browser automatically deletes them when they become too old, but also in the encrypted value itself so that the web server can kick the user back out to a login form if the session has expired.
But the problem of forced expiration comes back in: what if the user logs out, or ends a remote session? During logout, the server can tell the user’s browser to drop the cookie, but if an attacker were to somehow replay that cookie afterward then it would still be valid in the eyes of web servers. This is because there’s no central authority to dictate that a session was ended before the intended expiration date. If you allow users to log in once every 30 days, a large number of cookies could be floating around after explicit logout, ready to be replayed. If you restrict a cookie to live for only an hour or two, your users may be annoyed by having to log back in all of the time.
Additionally, remote session revocation is flat out impossible. Even if your web application kept track of each login operation by a user, it can only tell the user that these sessions may still exist based solely on their expirations. If another session was explicitly logged out, it may not know that. And the user would be unable to end a remote session early because the web servers don’t check in with that session store.
OAuth JWT Access Tokens
Similarly OAuth JWT access tokens are a solution. These JWTs are self-signed blobs that can also be verified locally. Your infrastructure needs to be configured to point to your authentication service to pull the JSON Web Key Sets (JWKS) needed to verify JWTs, but this is mainly needed only at startup. JWKS hold the public key(s) used to verify the JWTs. Otherwise, configuration includes making sure that expired JWTs are not allowed, and that the JWTs have the correct issuer and audience and other claims.
This is similar to requiring each server to have access to the secret key for cookie decrypting above. But it may be a bit easier knowing that the public keys are, well, public information and not another pain point to keep secure.
A JWT access token is minted by the identity provider (e.g. Auth0) for the user upon login using a standard OAuth flow. And they contain all of the information needed to specify that the user is authenticated, and which scopes the user is authorized for in the API. As a note, not all OAuth access tokens are JWTs, but in many of the major frameworks and services they tend to be.
By themselves, though, they have the same problems as encrypted cookies like with OWIN. Explicit logout and remote session revocation become difficult or impossible.
Enter Refresh Tokens
This is where OAuth refresh tokens come in to play. When a user authenticates with an identity provider, the provider grants your web application both a JWT access token and an opqaue (typically) refresh token which can be used to create more access tokens in the future.
During normal operation, a client (such as a native app or an SPA, or a service making calls to another service as a user) which holds onto an access token + refresh token pair (which I’ll shorten to token pair) will make API requests to a service by passing the access token as an Authorization Bearer token. If that access token is close to expiring, it will use the refresh token to mint a new access token with the identity service in the background, and subsequent API calls use the newly minted access token.
We now have a situation where an web API layer can still be detached from a session store for normal token verification, and the onus is on the client app to reach out to the identity provider infrequently to keep that locally-verifiable access token from expiring. The OAuth specification stipulates that tokens, access or refresh, can be revoked. In practice though, if your identity provider grants JWT access tokens, it’s more likely that they only support refresh token revocation. In this scenario, if a user or client logs out that process can include revoking the associated refresh token so that future attempts to mint a new access token fails.
This lends itself well to remote session revocation as well: if the identity provider has APIs to access active refresh tokens for a user then those can also be revoked. You may have that “a ha!” moment: the latest access token retrieved from a newly revoked refresh token is still valid, much like a non-expired encrypted cookie! This is why many OAuth identity providers stipulate that you should keep your access token lifetime rather short, no more than one hour usually. But the good news is that the user doesn’t need to re-login every hour! The refresh tokens can be long-lived to keep the user logged in for a good amount of time. Services allow various forms of refresh token expiration, such as absolute expiration time, a sliding expiration window with constant use, or a combination of the two. For example, a refresh token will expire after 30 days absolutely, but if not used for 48 hours it may expire early. This is possible because the refresh tokens are tied to a central data store that keeps track of this information; but again, this is only infrequently access by the client, not upon every web server request for verification.
Additionally, refresh tokens can rotate. With this configuration enabled in an identity provider, refresh token values only work once. When trading a refresh token for an access token, you get a new refresh token sent back as well to replace the old one. You’re effectively exchanging a token pair every hour, with one to provide access to an API and the other to exchange the pair again with the identity provider to keep it up to date. The purpose for this is added security: if your refresh token was leaked and a malicious third party uses it to mint a new access token, and then the legitimate application later attempts to use the same refresh token to get a new access token, the identity provider can automatically revoke the refresh token and end that authorized session early to protect from more access tokens being minted. This does, however, provide a short window where a malicious attacker can cause damage. But only if that leaked refresh token was recent and active; this minimizes the damage of historical refresh tokens that may be leaked in old logs.
Web applications
All of the above works well for, as specified: native apps, SPAs, or service-to-service setups. This is closer to the main purpose of OAuth as a whole: to let a user authorize Service B to access Service A as their Service A user. In the case of native apps/SPAs, it’s a little bit different in that the user has access to the tokens on their device or browser SPA instead of it being locked behind a different service.
With a web site/web application MPA however, this poses some problems. An authenticated browser page load can not send an Authorization: Bearer <token>
header in the HTTP request. It relies on HTTP Cookie headers to be sent instead. And typically, your login endpoint is the only one that sets the cookie for the browser upon login. If we simply store the token pair in the authentication cookie, what should happen when the access token is expired? The onus is usually on the client application (native app, SPA, service) to asynchronously keep that access token refreshed properly, in order to keep the API light in verifying authentication and separated from your identity provider.
The web application’s cookie verification middleware could be responsible for exchanging the token pair. It would detect that the access token is expired, get a new token pair from the identity provider, and mark the HTTP response with the new cookie value to replace the old one on the client browser. This method has some problems, though.
First, parallelism. Multiple requests can come through at the same time from a web browser. What if two requests are processed that both contain an expired access token? The web app would, in parallel, exchange the token pairs. With refresh token rotation enabled, this would trigger the protection mechanism when the second request attempts to exchange the token pair, rendering the first exchanged token pair unusable. Without refresh token rotation, it just becomes wasted resources as you’re hitting the identity provider two or more times when you only needed to do this once. A centralized locking mechanism within your infrastructure might help to alleviate this, by enforcing exclusive access to the identity provider in the context of a token pair, but then this becomes a centralized, important, and contested resource in your infrastructure which raises scaling concerns.
Second, metrics. Your web endpoints response times are no longer very useful. If any request to authenticated endpoints may or may not have to hit your (perhaps external) identity provider, it can add a lot of fluctuation to otherwise straightforward requests. Suddenly all of your endpoints' response times 5% highs are much longer and the variance increases a lot. This doesn’t seem like a fun time for internal analytics of your performance, given the random nature.
Another option is to have a little bit of javascript in your otherwise JS-free MPA website keep your cookie up to date, with a dedicated web app endpoint to handle cookie/token pair exchange. But if the website is open in multiple tabs, it would be difficult to prevent parallel requests going out and causing similar issues as the above. And not to mention that if a user opens their browser after more than an hour timeout period and hits the website, the first hit to the web server would contain an expired access token in the cookie, since the javascript never had a chance to refresh the token yet. The web server could potentially redirect the page load to the cookie token pair rotation endpoint and then get redirected back, perhaps only on GET requests. This still doesn’t feel right though. The user is still logged in validly, but the technical issue of rotation an OAuth token pair requiring you to not immediately respond with the page contents is unfortunate.
So where does this leave us? OAuth token pairs work, but only in the context of an application that controls access to an API and is able to refresh tokens asynchronously like through native apps, SPAs, or service backends. But web browsing in a traditional style does not allow for that luxury. So, storing the token pair in a cookie directly seems like a no-go. But there are alternatives to discuss further.
Pushed Revocation Lists
Let’s return to the concept of long-lived cookies with all of the identity and authentication information in them. This can be encrypted cookies like with OWIN, or even just long-lived access tokens (omitting refresh tokens). This brings us back to the same problems of being able to log out securely and avoid replay attacks.
One concept that I have not read about much is the concept of pushed revocation lists that allow for distributed and scaled authentication verified that doesn’t rely on a single session store. Essentially, when a user logs out of their session the web app not only deletes the cookie from the client browser but also announces to the infrastructure that this cookie has been revoked and should no longer be validated. It would do this by telling the session store about the revoked cookie, and the session store can push this information to all of the other web servers. The web servers don’t need to ask (or pull) this information from the session store on each authenticated request, keeping verification offline and quick. Instead, they keep an offline list of revoked cookies to do a very quick lookup against.
It would be recommended to not distribute the cookies themselves, but rather a hash of the cookie. This protects from potential leakage of actual cookies, and also helps keep the revocation list smaller in size. Additionally, you only need to store the cookie in the revocation list for as long as it’s still valid, so include expiration timestamps with the cookie hash and drop them from the list when they would be rejected for being expired regardless of the revocation list.
Implementation would benefit from an in-memory O(1) lookup mechanism, like hash tables or sets. And some mechanism to remove expired items after a while. Application startup would pull in the current revocation list from the source, and then subscribe to future additions to the list via something like a web hook subscription or a socket connection. The source of the revocation list should keep that information persisted, and subscribers can re-pull from the source every so often to account for push failures and generally keep this more fault tolerant.
I think I first came across this idea in the context of immediate logout of OAuth short-lived access tokens (in addition to revoking refresh tokens, of course). Revoking the refresh token with your identity provider means the refresh token no longer functions, but your access token is still valid for some time afterward. Adding the hash + expiration timestamp of the access token to a distributed revocation list is a pretty simple way of ensuring that replay attacks don’t work in the immediate term, for added security. And it works well with short-lived access tokens because the revocation list will only need to hold on to an particular access token hash for upwards of one hour or so. This leads to quick turnaround and keeps that revocation list physical size small. Within regular OAuth, this seems like a great idea.
List Scaling
With long-lived tokens/traditional cookies, however, you sacrifice memory size for quicker response times. When an auth cookie or access token has to live in the revocation list for long periods of time (such as up to a month) the server memory is going to be more constrained than normal. Millions of users, each with up to a handful of revoked sessions at any given moment, could easily start eating up memory on each and every web server or node in your frontline infrastructure.
It would be useful to find some current metrics of your own userbase logout behavior, and combine that with how much space an in-memory lookup table takes, and quantify more exactly what amount of memory is actually needed. It’s entirely possible that, for your use case, this works easily and without issue. But on the Amazon or Google scale, it may be untenable.
One slightly modified option is to delegate this revocation storage list to Redis cluster nodes, so there are fewer memory-intensive nodes in your overall infrastructure. Redis should be able to respond quickly, and auto-expire items for you. But if this is an option that scales well, then you could simply use this for use this for your session store in the first place as well.
As a quick aside, you could complicate things a little bit more by having several Redis clusters in your infrastructure that each get updates on revocation lists or session storage actions from a central store. The benefit would be to have more local (or I suppose the more recent, hip term is “edge”) Redis nodes that distribute well across various sub-deployments, so that a Redis cluster can expand over regions, for example. Like a hub-and-spoke model but for Redis clusters. It’s entirely possible that’s already available in Redis though! My knowledge of Redis clusters is a bit limited.
In Practice
If you want to go about actually implementing this, it might depend heavily on your identity provider. It would be great if your provider did some of the hard work of keeping track of that revocation list for you and supply web hooks or some other mechanism for updating live nodes with new revocations. But from my brief foray into OAuth providers, this doesn’t seem too common.
Auth0, for example, does have an API endpoint to blacklist access tokens, but it doesn’t work in practice. This requires a jti
claim in the access token (JWT ID) to identify the token and it’s basically become impossible to have Auth0 include that claim in any of your access tokens anymore. The API is largely there for historical reasons. Additionally, Auth0’s Actions (replacing their old Hooks and Rules) don’t seem to notify you of these revocations anyway, so you can’t realistically utilize this data.
Other providers may simply not have this type of revocation, or not have a sophisticated web hook system for notifications about revocations.
Okta looks like it may support this, actually! It’s OAuth /revoke
API supports both access tokens and refresh tokens, and their event hook lists app.oauth2.as.token.revoke
(“OAuth2 token revocation request”) as a supported event hook. Though, pulling a full revocation list may not be supported. Instead, it seems the main support is for the /introspection
OAuth endpoint to fail for revoked tokens.
So, in practice, you may be required to implement the persistent storage, the mechanism for pushing this revocation list, and the mechanism for pulling a full list at startup.
OAuth and Sessions
I think I’ve been side tracked with secure logout discussion, and forgot one of my other main points: session management. How does this work with OAuth? How does one view the other sessions that are active for their account when they authenticated with an OAuth identity provider? There must be some type of session storage, whether that’s checked continuously or only when viewing session information.
This will likely be up to which identity provider you are using, and what features it supports. The nature of OAuth refresh tokens implies that your authentication provider is stateful. The refresh token, at least in practice, is an opaque identifier that can have a sliding expiration window and immediate revocation. It’s only the access token that is stateless and verifiable offline. So, it would be great if your authentication provider exposed that information for you via the active refresh tokens. Since, after all, an active refresh token is mostly synonymous with a user authenticated session.
Unfortunately, OAuth as a spec doesn’t have much in the way of standardizing this. At most, it has the /revoke
endpoint but that’s limited to your current session. It makes sense not to expose this in normal OAuth as well, because if you used OAuth to let a website that uses social login, that website could potentially see all of your other social logins and revoke access remotely to those, logging you out of completely unrelated websites. Session management with OAuth is very much a first-party type of functionality.
Auth0, for example, does have a way of retrieving a user’s refresh tokens. This uses the Management API, as opposed to the OAuth API. Thus, you can control access to this with specific scopes for allowing access only to first party services and such, or hide it behind your own API layer that controls what you can see and revoke. However, this API is pretty light with information in terms of session management. It only stores a name of the device used to authenticate, derived from its user agent. It doesn’t store when that refresh token was first created, the IP address of the device used to create the session, etc. which may be useful to show to a user concerned about their account security.
Okta has some similar functionality. It allows the user to view all of their active refresh tokens for different OAuth clients. However, it’s even less featured than Auth0 in that it doesn’t even assign a name to these tokens. All you get is the token user and client identifiers, along with the scopes and when it was created and will expire. This is good information for functionality, but not the best for displaying to a user in a session management tab.
Additionally, it’s possible that your authentication provider does not allow this information to be retrieved at all. Though, thankfully, it does seem like most providers do have some kind of optional to log the user out of everything, either by just revoking all refresh tokens/sessions (without viewing them) or by revoking all user grants (which effectively disables refresh tokens as they can not issue a new access token without a valid grant to the client application).
Augmenting OAuth with Sessions
Let’s go over some options for augmenting your authentication provider with the ability to track some more details on active sessions, given the rocky landscape.
We’ll start with Okta. Okta, as mentioned, has a very richly featured event hook system. No doubt that an event hook is fired for when a user logs in and receives a refresh token of some sort. Thesse event hooks include when that took place, which IP address initiated the action along with automatically derived geographical data associated with that IP address, the user agent of the browser that initiated the login, and some more information. Using these event hooks, it’s possible for you to ingest these events to store and keep track of the details to emulate a session object which you can show to your user with more detail. For instance, the event hooks app.oauth2.as.token.grant.refresh_token
or app.oauth2.token.grant.refresh_token
exist for this purpose. While I don’t know from their documentation3 what all the details include specifically, I would assume that the identifier is included, along with the other information mentioned above.
With this, you can subscribe to these events and store them in your own database. When a user authenticates and a refresh token is created, you create an associated session in your store. When a user goes to a section in your web site to view their active sessions, you pull the information from your store. When a user deletes/deactivates/revokes one of these sessions, you have the refresh token identifier with which to revoke in Okta (or whichever authentication provider you’re using). When this is done, the refresh token stored on the other device will cease to work for obtaining a new access token, and the user is effectively logged out once that access token expires. This is why authentication providers suggest to use short-lived access tokens with long-lived refresh tokens in OAuth setups, so that in simple setups the logout does happen eventually and in a short-enough time period.
But we want to go the extra mile! Let’s dicuss revoking access tokens along with the refresh token in this situation. We’ll need to know some information about the access token which we wish to revoke, and those are typically only communicated between the authentication provider and the client. Using the same Okta event hooks, we are able to get events about access token grants. Admittedly, this does not contain the access token JWT in full. Looking at an example, I see there is a Hash (though of what is not clear), an expiration timestamp, and identifier of some kind which may work with Okta’s own revoke API as discussed earlier. For us, we want to be able to add a hash and an expiration timestamp to a distributed revocation list for our infrastructure to be able to continue with offline verification and not need to query Okta to see if the token is revoked or not which slows things down. If you know how the Okta event hash works and can use that in your revocation list, then you’re all set!
Though if you don’t have this information (or can’t verify how that hash in Okta works, like me after a brief search), you might have to choose a different method. Proxying your authentication provider is perhaps another solution, though with drawbacks. Acting as a man-in-the-middle proxy (or perhaps more of an API gateway) can have its issues with CORS and other such items. But if you’re able to tackle those, then you can keep track of OAuth API responses directly, and add your own hooks to retrieve all the details about the refresh and access tokens you need to be able to emulate sessions.
Some other things to keep in mind with emulating sessions on top of OAuth include expiration. With a refresh token, if it has an absolute expiration time, then you can auto-expire the emulated session on your end as well which is nice and simple. If your refresh token has a sliding expiration, that gets a bit more hairy because you might need to query your authentication provider’s API to see if that refresh token identifier is still active or not, either on a continual basis for all users or only when a user visits their session details page. Refresh token rotation may also pose its own challenges; it’s simple if the identifier for that token is static, but if that rotates as well with the token then you’ll have to continually rotate the identifier with the correct session while keeping the original created date/IP/geo data the same. And if you’re tracking access tokens with your session, those also rotate very often and you’ll want to keep track of all of them (dropping expired ones) as they rotate.
I think all of the above applies well to pure OAuth setups, such as native apps or SPAs. In an MPA cookie-based website, you’re acting more as the OAuth client instead and may have more control. Since you’re the one holding on to the token pair (perhaps in a cookie in plaintext, an encrypted cookie, or in session storage), keeping track of the sessions is a lot more straightforward because you can do with the data as you please, since you control the OAuth requests and responses directly. This is especially useful with session storage because you’re probably holding and controlling all of the session information including the current access and refresh tokens directly.
Or…
Or why not skip all of that?
If you’re dealing with a MPA with an external authentication/identity provider that deals with OAuth, perhaps you should just ditch the OAuth tokens as early in the process as possible. Lock down your provider so that only your MPA can access it (at least for that OAuth client), use it for OIDC, and when you get a successful identity token, extract the information you need from it and plug it into a new identity cookie for your MPA framework such as ASP.NET, Django, Rails, etc.
Those frameworks should already have a sane way of session management, in some sort. Some of them might use encrypted cookies that are hard to logout securely. Others might be fully backed with a live session store. And you’re converting OAuth/OIDC token lifetimes and constraints into a more native cookie lifetime/constraints. But it’s clear that OAuth wasn’t made for cookies and javascript-less MPA websites. Getting them to work that way is asking for trouble in some respect or another.
The exception might be if you just use long-lived access tokens without refresh tokens. That’s basically an encrypted cookie that exposes its information to the user or snoopers. Just see if you can add a revocation list on top of it, if possible.
Takeaways
Here are some general guidelines and facts to consider.
If you’re building a native app or an SPA then the best recommendation and most common solution seems to be OAuth with access token + refresh token pairs, refresh token rotation enabled, short-lived access tokens, client-side asynchronous token exchange, and refresh token revocation upon logout. This is closer to OAuth’s true purpose, and a lof of the technology and protocol was created with these scenarios in mind. Short-lived access token revocation is also an option, with a bit more custom infrastructure to keep track of that revocation list. And you have options to add remote session management on top of this in some fashion, though it will likely be custom and dependent on your authentication provider.
If you’re building an MPA web application, it gets more complicated, and it might likely depend a lot on your authentication provider.
If you’re using something like Ory Kratos which is in charge of creating the cookies and you query their API to get the session information, then you’re basically all set. It requires knowing how to scale up Kratos appropriately and trust that it will be performant with response times. And I think that you can add Ory Hydra on top of Kratos to get an OAuth server if you wish to add a native app alongside your MPA.
If you’re using something like ASP.NET session-storage-backed cookies, you’re in the same boat as Ory Kratos above. Storing the sessions in something like a Redis cluster may yield some better performance than a SQL database.
If you’re using something like ASP.NET/OWIN and are dealing with self-contained encrypted cookies without a session store or OAuth, you mainly need to worry about cookie revocation lists of some sort for user logout, and add a layer on top of this for your own remote session management.
If you’re using an OAuth-, JWT-based authentication provider and have to deal with access token + refresh token pairs in your MPA, convert them into more native cookie-based sessions with your chosen MPA framework. Don’t try to use access tokens as cookies! That’s not what they’re made for and you’ll run into issues attempting to get them to work like SPAs.
Note that short-lived acces tokens with rotating refresh tokens are actually really good for security. If an access token is leaked in some kind of log, the majority of those tokens will no longer work once they’re accessed by an attacker because they’ve already expired. And the refresh tokens no longer work either. With the tokens constantly refreshing, it adds a nice little bit of extra security. But in session-backed MPAs, immediate revocation is also helpful .
In nother article it might be fun to talk about Sender Constraints with tokens/cookies4.
-
https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/06-Session_Management_Testing/06-Testing_for_Logout_Functionality ↩︎
-
https://www.theverge.com/2023/3/23/23653115/linus-tech-tips-youtube-hack-crypto-scam ↩︎
-
https://developer.okta.com/docs/reference/api/event-types/?q=refresh ↩︎
-
https://auth0.com/blog/identity-unlocked-explained-episode-1/ ↩︎