Refresh Tokens

Overview


Investigate refresh tokens in FOLIO and propose an implementation plan.

StatusFunctionalityNotesStory
(tick)Ability to get a valid refreshTokenPOST /refreshtoken - requires "secret" permission not mentioned in the module descriptorAlready done
(tick)Ability to get a new access token via valid refresh tokenPOST /refreshAlready done
(error)Ability to revoke a refresh tokenSee Ability to Explicitly Revoke a RefreshToken Not needed
(error)Ability to revoke ALL refresh tokensMay not be urgent - if needed restart the auth module(s) with a new signing key.  See Ability to Explicitly Revoke a RefreshTokenNot needed
(error)Configurable access and refresh token expirationBoth are hardcoded - 10min/24hrs

MODAT-65 - Getting issue details... STATUS

(warning)Access token expirationSet in some cases but never checked

MODAT-64 - Getting issue details... STATUS

(tick)Refresh token expirationRefresh tokens that are expired are considered invalidAlready done
(error)Validation that a refresh token was generated by this FOLIO InstanceRight now depends on signing key.  If we go with rotating refresh tokens (and keys) this is no longer an issue.Not needed
(error)mod-login-saml supports refresh tokensCurrently only returns an access token

MODLOGSAML-57 - Getting issue details... STATUS

(error)Gracefully handle access token expiration in module-to-module requestsSee Gracefully Handle Access Token Expiration in Module-to-Module Requests

MODAT-66 - Getting issue details... STATUS

(error)Ensure we're not caching access tokens in edge-sip2Can probably be wrapped into the existing story for handing token expiration/invalidation

SIP2-71 - Getting issue details... STATUS

(error)Silent refresh in edge-commonCurrently caches access tokens for a configurable amount of time

EDGCOMMON-22 - Getting issue details... STATUS

(error)Refresh token rotation upon useSee Refresh Token Rotation and Automatic Revocation Upon Multiple Uses

MODAT-67 - Getting issue details... STATUS

(error)Automatic revocation of refresh tokens when used more than onceSee Refresh Token Rotation and Automatic Revocation Upon Multiple Uses

MODAT-67 - Getting issue details... STATUS

(error)Silent refresh in stripesProbably actually in stripes-connect

STCON-101 - Getting issue details... STATUS

(error)Disable use of JWE by default for refresh tokensSigning, but no encryption. See To Encrypt or Not to Encrypt?

MODAT-68 - Getting issue details... STATUS

(error)Refactor/Combine access/token endpointsSee Combine /token and /refresh endpoints in mod-authtoken?

MODAT-69 - Getting issue details... STATUS

JIRA

Spike:

Related:

Backend

Gracefully Handle Access Token Expiration in Module-to-Module Requests

  • Always check access token expiry during authorization - (question) 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

authtokenPOST/tokensclaimstokens

auth.signtoken

auth.signrefreshtoken

Generate and return access and refresh tokens 


claims

Property

Type

Default

Required

Notes

user_idstringNANo

UUID of the user these tokens are associated with

tenantstringNAYesThe tenant these tokens are associated with
substringNANoaccess token subject (username? module name?)
TBD



refreshTokenstringNANoOptional refresh token - if present, use this to 

tokens

Property

Type

Default

Required

Notes

accessTokenstringNAYes

Access token

accessTokenExpirationintegerNAYesAccess token expiration in seconds since epoch
refreshTokenstringNAYesRefresh token
refreshTokenExpirationintegerNAYesRefresh 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

Decisions

TBD


Appendix

Which APIs should clients use to refresh access tokens?

Several options were weighed:

  • (error) 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.
  • (error) 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
  • (tick) 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.