Refresh Tokens

Refresh Tokens

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

https://folio-org.atlassian.net/browse/MODAT-65

Access token expiration

Set in some cases but never checked

https://folio-org.atlassian.net/browse/MODAT-64

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

https://folio-org.atlassian.net/browse/MODLOGSAML-57

Gracefully handle access token expiration in module-to-module requests

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

https://folio-org.atlassian.net/browse/MODAT-66

Ensure we're not caching access tokens in edge-sip2

Can probably be wrapped into the existing story for handing token expiration/invalidation

https://folio-org.atlassian.net/browse/SIP2-71

Silent refresh in edge-common

Currently caches access tokens for a configurable amount of time

https://folio-org.atlassian.net/browse/EDGCOMMON-22

Refresh token rotation upon use

See Refresh Token Rotation and Automatic Revocation Upon Multiple Uses

https://folio-org.atlassian.net/browse/MODAT-67

Automatic revocation of refresh tokens when used more than once

See Refresh Token Rotation and Automatic Revocation Upon Multiple Uses

https://folio-org.atlassian.net/browse/MODAT-67

Silent refresh in stripes

Probably actually in stripes-connect

https://folio-org.atlassian.net/browse/STCON-101

Disable use of JWE by default for refresh tokens

Signing, but no encryption. See To Encrypt or Not to Encrypt?

https://folio-org.atlassian.net/browse/MODAT-68

Refactor/Combine access/token endpoints

See Combine /token and /refresh endpoints in mod-authtoken?

https://folio-org.atlassian.net/browse/MODAT-69

JIRA

Spike:

Related:

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

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 

 

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

Decisions

TBD

 

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.

  • 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

  • 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.