Overview
Investigate refresh tokens in FOLIO and propose an implementation plan.
Status | Functionality | Notes | Story |
---|---|---|---|
Ability to get a valid refreshToken | POST /refreshtoken - requires "secret" permission not mentioned in the module descriptor | Already done | |
Ability to get a new access token via valid refresh token | POST /refresh | Already done | |
Ability to revoke a refresh token | See Ability to Explicitly Revoke a RefreshToken | Not needed | |
Ability to revoke ALL refresh tokens | May not be urgent - if needed restart the auth module(s) with a new signing key. See Ability to Explicitly Revoke a RefreshToken | Not needed | |
Configurable access and refresh token expiration | Both are hardcoded - 10min/24hrs | ||
Access token expiration | Set in some cases but never checked | ||
Refresh token expiration | Refresh tokens that are expired are considered invalid | Already done | |
Validation that a refresh token was generated by this FOLIO Instance | Right now depends on signing key. If we go with rotating refresh tokens (and keys) this is no longer an issue. | Not needed | |
mod-login-saml supports refresh tokens | Currently only returns an access token | ||
Gracefully handle access token expiration in module-to-module requests | See Gracefully Handle Access Token Expiration in Module-to-Module Requests | ||
Ensure we're not caching access tokens in edge-sip2 | Can probably be wrapped into the existing story for handing token expiration/invalidation | ||
Silent refresh in edge-common | Currently caches access tokens for a configurable amount of time | ||
Refresh token rotation upon use | See Refresh Token Rotation and Automatic Revocation Upon Multiple Uses | ||
Automatic revocation of refresh tokens when used more than once | See Refresh Token Rotation and Automatic Revocation Upon Multiple Uses | ||
Silent refresh in stripes | Probably actually in stripes-connect | ||
Disable use of JWE by default for refresh tokens | Signing, but no encryption. See To Encrypt or Not to Encrypt? | ||
Refactor/Combine access/token endpoints | See Combine /token and /refresh endpoints in mod-authtoken? |
JIRA
Spike:
Related:
- - FOLIO-1233Getting issue details... STATUS
- - FOLIO-2524Getting issue details... STATUS
- - UIU-1324Getting issue details... STATUS
Backend
Gracefully Handle Access Token Expiration in Module-to-Module Requests
- Always check access token expiry during authorization - What about token cache?
- Access tokens without a valid expiration will be rejected
- Access tokens generated for module-to-module purposes have a new expiration, i.e. they don't inherit the expiration from the incoming token
Silent Refresh in edge-common
- Check if a cached access token is expired (or will expire very soon) before using it.
- Save refresh tokens in-memory and use as needed
- If refresh token is expired, re-retrieve the credentials from secret storage and log in again
- Gracefully handle error responses - e.g. if a access/refresh token expires
Refresh Token Rotation and Automatic Revocation Upon Multiple Uses
In order to minimize the impact of a leaked refresh token, they should be limited to one-time use. We should detect when a refresh token is attempted to be used more than once. When that happens, that refresh token, and all other refresh tokens associated with it should be revoked. See https://tools.ietf.org/html/draft-ietf-oauth-security-topics-15#section-4.12.2 for a full description.
Storage:
- kid - key Id, UUID - PK
- key - signing key, random generated string e.g. 4P_s_7nB#7PtA@__2vvMn8fmszWA$n+R
- exp - seconds since epoch
- jti - refresh token id, UUID of the current refresh token - used to detect multiple uses of the same refresh token (see below)
Upon login:
- Create a refresh token
- Generate a new signing key, kid, jti, calculate expiration
- Add an entry to storage with this information
- Include kid, exp, jti in refresh token
- Create an access token using the same key
Upon POST /refresh
- Perform validation (e.g. check expiration, etc.). Fail operation if invalid. If valid but expired, remove entry from storage
- Check the provided jti against what's stored
- If the refresh token id matches
- Add/update the current jti
- Reuse the kid/exp/key to generate and issue a new refresh token
- Else
- Remove the entry and fail operation (effectively revoking all tokens issued with that key)
Upon authorization
- get the appropriate signing key from cache/storage and verify the access token
- Check the access token expiration
- If not expired, continue
- Else, fail the request
Upon revoke
- Remove entry from storage
Periodically
- Prune expired entries - TODO - what's the mechanism for this?
Craig McNally TODO - provide one or more examples of this flow
Possible Alternative: If we don't want to generate/store new signing keys as described above we could simplify this by replacing kid/key with a "grant id" (gid). All refresh tokens would use the same signing key that's used for signing access tokens.
Ability to Explicitly Revoke a Refresh Token
We need to add the ability to manually revoke a given token. Currently the method that checks if a refresh token is revoked is stubbed out and always returns false.
- Not sure if we need an API for this, or if it's simply good enough to have a way to "manually" do this by direct interaction with storage
- Remove the key entry from storage will effectively revoke the refresh token (actually all refresh tokens issued with that key)
- Simply use the refresh token you want to revoke. If it has already been used, the refresh token and all associated refresh tokens will be revoked. Using the refresh token twice will force this to happen immediately.
To Encrypt or Not to Encrypt?
Currently the refresh tokens issued from mod-authtoken are encrypted (JWE). I'm not sure that's necessary as there doesn't appear to be anything sensitive/secret in the token itself. Unless there's a compelling reason to encrypt these, I suggest we save the time/resources on the extra crypto and forego the use of JWE.
They still need to be signed.
Combine /token and /refresh endpoints in mod-authtoken?
There are currently separate endpoints for obtaining an access token and obtaining a refresh token. Since these two tokens will go hand-in-hand, it will simplify things if the two endpoints were combined.
Interface | Method | Path | Request | Response | Permissions Required | Description | Notes |
---|---|---|---|---|---|---|---|
authtoken | POST | /tokens | claims | tokens | auth.signtoken auth.signrefreshtoken | Generate and return access and refresh tokens | Proxy to storage module. |
claims
Property | Type | Default | Required | Notes |
---|---|---|---|---|
user_id | string | NA | No | UUID of the user these tokens are associated with |
tenant | string | NA | Yes | The tenant these tokens are associated with |
sub | string | NA | No | access token subject (username? module name?) |
TBD | ||||
refreshToken | string | NA | No | Optional refresh token - if present, use this to |
tokens
Property | Type | Default | Required | Notes |
---|---|---|---|---|
accessToken | string | NA | Yes | Access token |
accessTokenExpiration | integer | NA | Yes | Access token expiration in seconds since epoch |
refreshToken | string | NA | Yes | Refresh token |
refreshTokenExpiration | integer | NA | Yes | Refresh token expiration in seconds since epoch |
Refresh endpoint in mod-login?
In order to avoid proliferation of modules dependent upon the authtoken interface, we should create an endpoint in mod-login which clients can use to refresh their access token. Other options that we considered are documented in the Appendix.
Since stripes/stripes-connect will likely store refresh tokens in a httpOnly cookie, this new refresh endpoint will accommodate two mechanisms for communicating refresh tokens, or "tokenTransport":
cookie - refresh tokens are sent as a cookie and new tokens are furnished via set-cookie.
POST /authn/refresh HTTP/1.1 Cookie: rtok=123 Content-type: application/json { "tokenTransport" : "cookie" } HTTP/1.1 200 OK Set-Cookie: rtok=345, tok=xyz { "tokenTransport" : "cookie", "refreshTokenExpiration": "2020-04-18T12:52:54Z", "accessTokenExpiration": "2020-04-17T13:07:54Z" }
body - refresh tokens are sent in the request body and new tokens are furnished in the response body.
POST /authn/refresh HTTP/1.1 Content-type: application/json { "tokenTransport" : "body", "refreshToken": "123" } HTTP/1.1 200 OK { "tokenTransport" : "body", "refreshToken": "345", "accessToken": "xyz", "refreshTokenExpiration": "2020-04-18T12:52:54Z", "accessTokenExpiration": "2020-04-17T13:07:54Z" }
Note that in both cases the refresh and access token expiration date/times are explicitly returned in the response body. This is because the client will likely want to use this information to request a new access token before the current one expires. The refresh token expiration is probably less useful, but will help clients know that their refresh token is expired before having them call the refresh endpoint and then having to react to an error response. Instead they can just send the user directly to the login screen to re-authenticate.
NOTE: the mod-users-bl login endpoint is actually what stripes calls. Similar changes will need to be made there as well.
Frontend
Use Refresh Tokens in Stripes
- There's s Spike for this in the stripes-connect project (link in overview table).
- Details TBD
Open Issues
- Look at existing OSS - https://spring.io/projects/spring-security, https://www.keycloak.org/
Appendix
Which APIs should clients use to refresh access tokens?
Several options were weighed:
- Clients call mod-authtoken's refresh endpoint directly (e.g. POST /refresh)
- Pros:
- No changes are needed to mod-login
- Cons
- Proliferation of dependencies on mod-authtoken. Edge APIs, Stripes, etc. will now need to call mod-authtoken directly, something not previously needed.
- Pros:
- Clients provide a refresh token when calling mod-login's login endpoint (e.g. POST /authn/login)
- Pros:
- No new endpoints required
- Clients always call the login endpoint, either with credentials or a refresh token
- Cons
- Overloading the login endpoint is confusing and messy - breaking changes
- Pros:
- Clients provide a refresh token when calling a new refresh endpoint in the login module (e.g. POST /authn/refresh)
- Pros:
- Distinct endpoints for login and refresh is cleaner - and makes it clear what the purpose of each is
- The existing login endpoint doesn't necessarily need to change, though it might be changing anyway for FOLIO-2523
- Cons:
- Clients need to know which endpoint to call, depending on whether they're furnishing credentials or a refresh token.
- Pros: