Overview and Use Cases
This document will outline the design and implementation of OAuth 2.0 refresh tokens and token revocation in Synapse.
Jira Epic: - PLFM-4585Getting issue details... STATUS
Refresh Tokens: - PLFM-5753Getting issue details... STATUS
Token Revocation: - PLFM-6120Getting issue details... STATUS
Refresh Tokens
Refresh tokens would allow a 3rd party client to request a new access token from Synapse. This enables OAuth clients to have long-lived access to a user’s identity and resources. Refresh tokens are an optional component defined in the OAuth 2.0 specification (https://tools.ietf.org/html/rfc6749#section-1.5).
Use Cases for Refresh Tokens
Support for Workflows. Workflows that act on behalf of a user may have a long queue of jobs. If the duration of those jobs exceeds the duration of an access token (currently 24 hours), then the job will fail. A workflow triggered by a Synapse OAuth client could utilize a refresh token to maintain authorization beyond the current duration of an access token.
OAuth in the Synapse Python Client. While not currently implemented, one goal is to allow users to use OAuth to authenticate in the Python client because it is more secure than a username and password, or an API key. Because access tokens are short-lived, a user authenticating into the Python client with our current implementation of OAuth would be forced to reauthenticate every 24 hours. A refresh token could be stored locally, so that users would not be required to reauthenticate.
Token Revocation
Because refresh tokens allow a user to issue long-lived access to a 3rd party client, we should allow users and clients to revoke this access. This gives a user more control over their data, and additional services allow a user to audit their granted permissions so they may re-evaluate the services with access to their data. In cases where a client no longer needs access to a Synapse user’s resources, they may revoke the token in order to prevent future unauthorized access.
User-centric token revocation is not defined in any of the OAuth Specifications. The OIDC Specification § 16.18 simply suggests it: ““…The Authorization Server SHOULD provide a mechanism for the End-User to revoke Access Tokens and Refresh Tokens granted to a Client.”. Thus the design for this feature is influenced by other services, such as those showcased by Okta/OAuth.com on their page Listing Authorizations https://www.oauth.com/oauth2-servers/listing-authorizations/ .
Use Cases for user-centric token revocation
A user may no longer need to use an OAuth client that has been granted access to their Synapse identity and resources
A user may no longer trust an OAuth client that has been granted access to their Synapse identity and resources
Client-centric token revocation is defined in RFC 7009 https://tools.ietf.org/html/rfc7009. From the RFC:
From an end-user's perspective, OAuth is often used to log into a certain site or application. This revocation mechanism allows a client to invalidate its tokens if the end-user logs out, changes identity, or uninstalls the respective application. Notifying the authorization server that the token is no longer needed allows the authorization server to clean up data associated with that token (e.g., session data) and the underlying authorization grant. This behavior prevents a situation in which there is still a valid authorization grant for a particular client of which the end-user is not aware. This way, token revocation prevents abuse of abandoned tokens and facilitates a better end-user experience since invalidated authorization grants will no longer turn up in a list of authorization grants the authorization server might present to the end-user.
Use Case for client-centric token revocation in context
Resource access is no longer needed by the client. As an example, a workflow engine may no longer require access to a user’s Synapse account at the conclusion of a job. They may revoke the token to ensure it is no longer valid.
Revocation Model
Currently, only short-lived access tokens are minted in Synapse, and cannot be revoked because they are not stored in the database.
Since we will be issuing long-lived refresh tokens, we will need a mechanism to revoke refresh tokens. While not necessary, it would be ideal to also revoke the access tokens themselves. (RFC 7009 § 2)
To accomplish this, we can store refresh tokens in the database, which can be revoked by a user or client with a REST API call. Once the refresh token is revoked, it may no longer be used to generate access tokens.
By linking an access tokens to its associated refresh token, we are able to invalidate access tokens without storing them in the database.
Linking Access Tokens to Refresh Tokens
Access tokens are JWTs. To link an access token to a refresh token, we can simply add a claim with a corresponding refresh token ID. The JWT specification § 4.2 suggests we use a namespace for this claim, (e.g. Auth0 recommends a URL like https://synapse.org/refresh_token_id
or https://sagebionetworks.org/refresh_token_id
, but we should be able to use org.sagebionetworks.repo.model.oauth.claims.refresh_token_id
). As a side note, I think we are already in violation of this specification, since we currently use nonstandard, non-namespaced claims such as orcid
, is_certified
, etc. We should determine if we should get back “in-spec” and add namespaces to the existing claims (breaking API change).
When we verify the access token, we can deserialize the JWT and make a database call to see if the corresponding refresh token is revoked. If the refresh token is revoked, we reject the request.
Similarly, when an access token is revoked, we just revoke the refresh token (RFC 7009 § 2.1: “If the token passed to the [revocation] request is an access token, the server MAY revoke the respective refresh token as well.”)
Since we are now storing refresh tokens in the database, we can link them to the user and show them when the user wants to see them. Since access tokens are directly linked to refresh tokens, we need not show them (under this design, we couldn’t because we don’t even store them)
Additional Scope: offline_access
Per OIDC Core 1.0 § 11, we should only permit the use of a refresh token when a client requests the offline_access
scope.
When offline_access
is not requested, we can issue only a short-lived access token that is not associated with a refresh token (this is the current behavior of the system).
(Aside: the specification only requires that without the offline_access
scope, a refresh token may not be used to issue tokens that access identity. The specification does not dictate that we could not otherwise use a refresh token. For the sake of simplicity, we can apply this to all permissions and only issue refresh tokens when offline_access
is approved.)
REST API
This section will identify a new object used in the REST API, three new endpoints, and an extended implementation for an existing endpoint.
New objects
There are two new objects proposed in this document.
OAuthGrantedPermission
This object can be used to show the user the OAuth clients that have access to the requesting user’s resources and identity. Using this information, the user can identify the client that has access, the amount of access that the client has (via scopes), how long the client has had access, and how recently the client has accessed that user’s resources by requesting a new access token.
OAuthGrantedPermission
client: OAuthClient (client information that can be displayed to the end user)
scopes: Array<OAuthScope> (The scopes that the client can request using the issued access tokens)
authorized_on: date-time (This is the time when access was first granted)
last_used: date-time (This is the most recent time the refresh token was used to issue a new access token)
OAuthTokenRevocationRequest
This object is used when a client makes a request to revoke a refresh/access token. It is defined by RFC 7009 § 2.1.
OAuthTokenRevocationRequest
token: string
token_type_hint: enum { access_token, refresh_token }
New API Endpoints
Three new endpoints and an extension of implementation for one existing endpoint are proposed.
Viewing applications that have OAuth access to a user’s account
Endpoint: GET /oauth2/permissions/
Request body: none
Return body: PaginatedList<OAuthGrantedPermission>
Returns a paginated list of the clients and permissions that the user has granted. Allows a user to audit which parties have access to their resources.
User revocation of a client’s access
Endpoint: POST /oauth2/permissions/revoke
Request Parameter: client_id
: the OAuth2 client that will no longer have access to the user’s resources and/or identity
Response: On successful revocation, return HTTP 200. No body.
Upon calling this method, the refresh token and access tokens held by the specified client for the authenticated user making the API call will be revoked.
Client revocation of a token
Endpoint: POST /oauth2/revoke
Request Body: OAuthTokenRevocationRequest
Response: By RFC 7009 § 2.2, on successful revocation, HTTP 200. No body.
Upon calling this method, the refresh/access token and associated tokens held by this client and associated with the user are revoked. Note: a specific path for this endpoint is not named by OAuth 2.0/OIDC specifications.
Requesting a new access token with a refresh token
Endpoint: POST /oauth2/token
This method exists. This feature proposal would add support for grant_type=refresh_token
, and return a refresh token for grant_type=code
. For details, see OIDC Core 1.0 § 12.1, 12.2.
Backend Implementation Detail: Database Model
This section covers implementation details that will not be visible to users of the new services, and is not necessary to read to have an understanding of how to use the new services.
To support revoking access tokens, we will need to track refresh tokens in the database
New DB Table: OAUTH_REFRESH_TOKENS
id: integer, primary key
token: CHAR(36) (semantically, a UUID)
created_on: TIMESTAMP
user_id: BIGINT (referencing the principal user)
client_id: BIGINT (referencing the client)
last_used: TIMESTAMP
Uniqueness constraint on (user_id, client_id)
One implication of this design is that a client may only have one active refresh token per user. This seems to be how most OAuth providers design their systems (clients only appear once in lists of granted access). It also reduces user confusion (“Which instance of Client X permissions do I want to revoke?”)
FAQ/Anticipated Concerns
Does adding refresh tokens break any existing behavior?
It should not because we are merely extending the access token with a reference to its refresh token ID. Current clients would not see the refresh token without requesting the offline_access
scope, and if they did receive it, they may ignore it. The duration of access tokens is unchanged.
Open Questions
Is a UUID a good choice for the refresh token?
Should a refresh token expire?
Per sections 1.5 and 10.4 of the OAuth 2.0 spec (by way of this StackOverflow post), it seems we have some liberty in terms of the lifecycle of a refresh token.
Some options we have include
Refresh tokens last until they are revoked
Simple
Rotate the refresh token after each use
Trades additional client complexity for security
Refresh tokens expire after a duration
User will be required to reauthorize the application after a certain period of time (e.g. one year)
Functionally, this is not much different than a long-lived access token, so I don’t think this is the best option
This decision should be driven by use-cases
Should a client be able to possess more than one refresh token per user at a time?
The proposed API design for user revocation of a token assumes “no”, because a user revokes access using the client ID.
Other applications seem to only allow one token per user (listing your granted application integrations in Google, GitHub, etc. won’t show the same client more than once, as far as I know)
As a user, revoking a token is confusing if you have more than one token per client (“Which instance of Client X permissions do I want to revoke?”).
What happens when a client attempts to create a new refresh token, when one exists (e.g. to ask for more scope)
Probably just invalidate the old refresh token, and create a new one
Is there a compelling use case for a user to be able to see the access they have given in the past but have revoked? (i.e. do we keep a record of revoked refresh tokens?)