Thinking about how to secure the refresh-token for the web gave me an idea for what I think would be a pretty good set up for issueing JWT access-tokens and refresh-tokens.
refresh-tokens are issued by a central stateful server (backed by a DB), that maintains a black list.
- black list occasionally runs a "job" to remove any entries that have expired, keeping it light and lean
- refresh-tokens are only available at a specific domain, so that we can use secure cookies for the web
access-tokens are issued where ever you want (non centralized, only need a secret).
- have a small TTL (< 30mins)
- can be used across domains
- can be obtained with either a username:password pair, ssh-key, OR a valid refresh-tokens
- when an access token is obtained using either username:password or ssh-key, a request is made also made for a refresh-token, passing the credentials and proxying the response (along with secure, httpOnly cookies) which should contain the refresh-token
- do in fact have their scopes stored in the token
Here's the interesting twist: ** Refresh tokens are somewhat useless on their own, but require being paired with a valid, but possibly "recently-expired", access-token (what constitutes "recently-expired" should be configurable), to get a NEW access-token. **
When an API call is failed due to expired-auth, if a refresh-tokens is available, a request for a new access-token is made on behalf of the user, passing along the expired access-token, and if successful and a new access-token is provided, the original request is made again with the new access-token.
If there is no refresh-tokens provided, or the refresh-tokens has expired, the request fails and the user is "logged out" (both refresh token and access-token are purged), and will need to re-authenticate with a username:password pair or ssh-key.
refresh-tokens must be stored securely, for the platform, as they basically allow eternal access-tokens.
For the web, refresh tokens are stored in secure and httpOnly cookies, inaccessible from JavaScript (and XSS), but ultimately useless without an access token (even an "recently-expired" one), since CSFR attacks don't allow you to get the response (as far as I understand) from the access-token issuer, your only concern would be the concerns that have alway been there: a successful XSS attack could allow access for as long as the access-token was valid (mitigated a bit by short TTL), and any changes in scope would only take affect after the current access token expired (as access-tokens can't be revoked like refresh-tokens can).
Maybe this could even be modified to have API endpoints always issue a new token on every successfully authenticated request, limiting the scope problem to one request rather than a half-an-hour.