I've got an OAuth2-driven authorization system for the applications in my ecosystem using the Auth Code flow. At the moment, it is working extremely well following what I feel are best practices. My current flow can be summed up as this:
The problem with this approach is it is heavily dependent on a single-backend/monolith/etc architecture. When the access token expires, the backend app in question needs to be able to get the refresh token and then do the refresh against the Auth Server.
My goals for the future state of this architecture are driven by two main concerns:
My initial thought was to move the refresh handling to the client-side part of the architecture. Since all the apps are owned by me, I could setup a new flow where a user would login directly with the Auth Server, and then the Access (JWT) and Refresh tokens are set as HTTP Only cookies. This is further supported by the fact that all of my apps are hosted via a single domain name (with different root URI paths for different apps). The new token structure would determine which apps a given user has access to.
My concern there is how to secure the Refresh Token client-side. Most likely I would need to use the Path attribute and depend on the fact that all my apps are on the same hostname, which as I said they are and that will not be changing.
I guess the point of this post is I'm looking for guidance on best practices for handling this kind of scenario.
A few thoughts based on design patterns we recommend at Curity:
BACKENDS AND JWTs
Related APIs should be able to forward JWT access tokens to each other, as detailed in the scopes article. Validating the JWT in each API results in a recommended zero trust setup.
BACK END FOR FRONT END
We recommend a particular way of doing this, focused on separation of web and API concerns - see this code example on the token handler pattern.
Token refresh is handled via HTTP Only cookies and client side retries. This provides a stateless solution that is easy to manage, and where the web back end is static content only, eg a content delivery network. It requires a couple utility API components to be deployed though.
An alternative option, as you say, is to write code within your back end to store tokens, so that cookies only contain a Session ID.
COOKIES AND MULTIPLE WEB APPS
In our resources we use reverse proxies / API gateways as the entry point to APIs, rather than a web back end. Common plumbing such as translating cookies to tokens can then be managed via gateway plugins - as covered in this tutorial.
However, when you have multiple web apps you need to keep cookies isolated during API requests. Each app therefore needs its own API routes. This is awkward but is a consequence of needing to use secure cookies for best browser security. It is best managed in the gateway, and separate domains or subdomains is usually cleanest:
Each web team is then responsible for their own API routes and the cookie / CORS / CSRF stuff, rather than the API developers.
SSO
This should be determined solely by the Identity Provider session cookie, which will remain valid as you navigate across apps. If you are presenting a login button when the app does not have a secure cookie yet, then it will not feel like SSO though. One technique for making SSO look seamless is for each app to automatically redirect when it does not have application cookies yet, rather than presenting a login button.
SUMMARY
A long answer, but the best solutions require separation of concerns that is not always obvious. Done well, the end result should be simple code in apps, correct security and an architecture that can be scaled.