Background
There are currently many ways to authenticate a request to Synapse, including
Session tokens. Session tokens grant access to all account functions. Session tokens expire after 24 hours, and they can also be refreshed (for an additional 24 hours) and revoked. To acquire a session token, users must either
Enter their username and password
Sign in using Google as an OIDC identity provider.
API keys. API keys grant access to all account functions. API keys do not expire, but they can be revoked, and a new key can be generated. Users may only have one API key active at a time. A user may retrieve their API key at any time (and as an implication, they are stored unhashed in Synapse).
Using an access token. Access tokens last 24 hours. Access tokens are also scoped, so that a token may only be used to perform specific types of actions. Only verified OAuth 2 clients may be issued access tokens. At this time, access tokens cannot be revoked unless they are issued with a refresh token.
There are two ways that a client can obtain an access token, both requiring client credentials.
Sending Synapse a valid authorization code, which is granted when a user authorizes the OAuth client. Authorization codes expire after one minute.
Sending Synapse a valid refresh token. The refresh token is granted when the user authorizes an app with the
offline_access
scope. Refresh tokens expire 180 days after being issued and are single-use, though a new refresh token is issued when one is used, so access should be considered non-expiring. Refresh tokens can be revoked, and access tokens associated with/granted by the refresh token will also be revoked. Additionally, we only store the hash of the refresh token, so the token can only be retrieved at the time it is generated.
Table Summary
Session Token | API Key | OAuth 2.0 access/refresh token | |
---|---|---|---|
Access Granted | Entire account | Entire account | Scoped, defined by the client |
Expires | 24 hrs after being issued | Forever | Access: 24 hrs after issued Refresh: 180 days after issued |
Revocable | Yes | Yes | Only if associated with refresh token |
Maximum that can be issued | ? | 1 | Access: Unlimited Refresh: 100 per user per OAuth client |
Stored as/Re-retrievable | ?/? | Unhashed token/Yes | Access: Not stored/No Refresh: Hashed token/No |
Motivation
We can reduce the complexity and attack surface of authentication with Synapse by utilizing OAuth 2 login flows in places where other mechanisms are used.
In particular, we can aim to replace usage of the API key with OAuth refresh tokens. The most common use case for the API key is to authenticate Synapse command line clients. Using refresh/access tokens in place of an API key provides a couple of advantages:
Timed expiration - if a particular session isn’t used for 180 days, the token expires
The ability to grant/revoke access on the machine-level, instead of having one key used everywhere
Scoped access, in cases where the command line app is running a job that only needs a subset of access/functionality
Using either the authorization code (currently supported) or device code (not currently supported) OAuth 2 flows require users to login on Synapse.org via a browser, instead of entering credentials at the command line, or pasting an API key into a config file.
While currently a lower priority, we can also consider the ability to replace the usage of session tokens with access tokens in the GWT client as the backend requirements are very similar.
The caveat to using OAuth 2.0 for native apps and JavaScript-based SPAs is that in these circumstances, the OAuth client cannot securely and confidentially store credentials. This scenario is a use case that is outlined in the OAuth 2.0 specification, but we currently expect all Synapse OAuth clients to be confidential clients. Therefore, we must consider the security and access implications of creating/allowing public OAuth clients.
Requirements/Notes/Considerations
Each point in this section will be fleshed out with reasoning, pros, cons, etc. and then consolidated once confident in what’s necessary.
PKCE
TODO
Can’t require it from all clients bc breaking API change
Require it just for public clients?
Designating new clients as public/confidential
OAuth 2.1 suggests doing so:
Except when using a mechanism like Dynamic Client Registration [RFC7591] to provision per-instance secrets, native apps are classified as public clients, as defined in Section 2.1; they MUST be registered with the authorization server as such. Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly.
Public clients may or may not be issued credentials
RFC 6749 informs us that client credentials are not useful for public clients:
The authorization server MAY establish a client authentication method with public clients. However, the authorization server MUST NOT rely on public client authentication for the purpose of identifying the client.
OAuth 2.1 suggests that they shouldn’t be required, so why issue them?:
Secrets that are statically included as part of an app distributed to multiple users should not be treated as confidential secrets, as one user may inspect their copy and learn the shared secret. For this reason, it is NOT RECOMMENDED for authorization servers to require client authentication of public native apps clients using a shared secret, as this serves little value beyond client identification which is already provided by the "client_id" request parameter.
The only legitimate case to issue secrets is if we have services for managing the OAuth client that require client credentials. However, in most cases, these services should be used by the user that owns the client, rather than the client itself.
Public clients may be required to use certain grant types
Currently, the only grant types implemented are the more secure possible grant types, likely meaning no restrictions at this time.
Users may need to explicitly opt-in to allowing public client access to their resources (e.g. a simple checkbox and supplemental info modal on their profile settings page)
Notes on public clients using refresh tokens
OAuth 2.1 Note on refresh tokens for public clients:
Authorization server MUST utilize one of these methods to detect refresh token replay by malicious actors for public clients:
* _Sender-constrained refresh tokens:_ the authorization server cryptographically binds the refresh token to a certain client instance by utilizing [I-D.ietf-oauth-token-binding] or [RFC8705]
* _Refresh token rotation:_ the authorization server issues a new refresh token with every access token refresh response. The previous refresh token is invalidated but information about the relationship is retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach. The authorization server cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token. This stops the attack at the cost of forcing the legitimate client to obtain a fresh authorization grant.
TODO: Look into implementation and costs/benefits of binding tokens to clients
Refresh token rotation is implemented, but we currently do not revoke an active token if an invalid token is used (so an attacker could hijack the session). Should we implement this? If so, should this be the behavior for confidential clients as well?