Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Note

The initial purpose of the document was to identify OAuth 2 as a solution for replacing API keys in the command line clients. We determined that OAuth 2 is not a good fit for the command line clients, so design work for that issue has been moved to Personal Access Tokens. OAuth 2 public client implementation has not begun, as of the time of writing.

This document still outlines requirements for OAuth 2 public clients, which may be worth implementing in the future.

Summary

Using OAuth 2 to authenticate with Synapse has many benefits over using older authentication mechanisms. We can try to consolidate authentication services to using OAuth flows wherever possible. The next step is to permit This document focuses on enabling the creation of OAuth 2 public clients, which would allow OAuth 2 based authentication in Synapse command line apps and third-party browser-based SPAs like the GWT web client. Additionally, this will hasten the deprecation of API keys.

To securely support OAuth 2 public clients, these actions are needed (backfill this list with Jira tickets when confirmed):

  • Support PKCE in the authorization code flow:

    Jira Legacy
    serverSystem JIRA
    columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-6337

  • Add field to label OAuth 2 clients as either “public” or “confidential”; backfill existing clients as “confidential”:

    Jira Legacy
    serverSystem JIRA
    columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-6338

    • Public clients should not be issued secrets

      • Note: Refresh tokens become single-use bearer tokens.

    • Public clients should be required to use PKCE

    • Don’t save consent records for public OAuth clients

  • Revoke the current iteration of a refresh token when an old expired/used refresh token is usedDon’t save consent records for public OAuth clients:

    Jira Legacy
    serverSystem JIRA
    columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-6339

To fully support Synapse command line clients, in addition to the above, we must:

  • Add a bootstrapped OAuth client for Synapse command line apps:

    Jira Legacy
    serverSystem JIRA
    columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-6340

  • Add service to issue a (user-authenticated) refresh token for the Synapse command line apps OAuth client:

    Jira Legacy
    serverSystem JIRA
    columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-6341

Background

There are currently many ways to authenticate a request to Synapse, including

...

  • Leased/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 necessaryThis section contains an explanation of each requirement for supporting OAuth 2 public clients in Synapse. This section is summarized at the top of this document, and services are enumerated at the bottom of this document.

PKCE

Best practices suggest that authorization servers are required to support PKCE [2][3]. PKCE only provides benefits to public OAuth clients, since a malicious app must have knowledge of an OAuth client’s credentials to execute the attack.

For more details, expand the following section:

Expand
titlePKCE in a nutshell

PKCE prevents a malicious application from hijacking an authorization code. This can occur when using a native app’s OAuth client, and the redirect URI is compromised by a malicious application observing traffic to the user agent (mobile app or web browser). In this scenario, the malicious app has the authorization code, and is in possession of the client’s credentials (if they exist), because it is a public client. Thus the malicious app now has access to the user’s account.

With PKCE, the client generates a high-entropy random string, and calculates the SHA256 hash of the string. When making the initial authorization request, the hash is sent to the authorization server. The authorization server associates the hash with the authorization code, before sending it to the client (the hash may be embedded in the authorization code)

When a client uses the authorization code, they must provide the initial random string. The authorization server computes the SHA256 hash of the string, and will only issue token(s) to the client if the hashes match.

Designating new clients as either public or confidential

...

In OAuth 2.0, the authorization server SHOULD NOT assume client type [1]. We can require client creators to specify their client type upon creation. This is a breaking API change, but it is acceptable because OAuth clients aren’t created programmatically (it wouldn’t make sense if they are, since they must be manually verified to be functional).

...

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

...

Use of an expired refresh token invalidates the active refresh token

The following would only apply to public OAuth clients because the refresh token acts as a bearer token. The goal is to reduce the risk and impact of token theft/replay attacks.

OAuth 2.1 ([2]) 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.

We will use refresh token rotation because it is partially implemented (we currently do not revoke an active token if an expired token is used). We can extend our implementation to include the missing behavior.

Token binding is complicated, so we won’t use it. For details, expand the following section.

Expand
titleToken Binding

Token binding introduces a public/private keypair and encrypts additional data to bind tokens to a TLS connection, thus a token could not be used if intercepted via MitM/token export. While powerful, implementing token binding can be complicated and has seen low adoption. We may choose to further investigate token binding as in [4] later. Token binding as described in [OAuth 2.0 Token Binding] does not seem to be an acceptable solution, because the draft has expired, and the mechanism to implement token binding on the client side does not seem to be supported by any major browsers (abandoned by Chrome, and only implemented in Edge before it became Chromium-based).

...

Refresh token rotation is implemented, but we currently do not revoke an active token if an invalid token is used. We should extend our implementation to include this behavior.

Risk in Saving Consent Records for Public OAuth Clients

...

A new attack mechanism is opened up if this same mechanism is applied to public OAuth clients, because the client credentials (if they exist) are not secrets--a malicious actor could use a public client that a user has already authorized to gain access to an account without a user’s consent.

To illustrate For an illustration of this scenario, consider:expand the following section:

Expand
titleSaved Public Client Consent Records Threat Example

Consider

  • Alice, an Synapse Python Client user who authorizes access to their account with OAuth. Alice is somewhat security-aware, and carefully checks the information she authorizes an OAuth client to have before granting it.

  • Mal, a malicious actor trying to gain access to user resources by taking advantage of the public OAuth clients in Synapse.

Because Alice uses the Python client, she has authorized the related public OAuth client to access all of her account resources accessible via OAuth. Synapse has saved an authorization record, so that Alice can quickly log-in in the Python client with minimal friction.

Mal has created an app that claims to save Alice a lot of time in her analyses, and has convinced Alice to attempt to log in to her app. Unbeknownst to Alice, the app uses the same OAuth client as the Synapse Python client, and requests the same set of scopes and claims. Because Synapse has saved her authorization record, Alice never has a chance to see that she is authorizing the “Synapse Python Client” OAuth client, which would tip off to Alice that Mal’s app is malicious. Mal now has unauthorized access Alice’s account.

New Services

Enabling public clients in general is required before meeting requirements for browserless command line apps, so the first set of requirements below must be met before beginning the second set.

Public clients capable of using authorization code flow

Public clients are first- or third-party OAuth apps that cannot reliably store a client secret. Currently, the only way to acquire OAuth access is through the authorization code flow, which requires a web browser. The following services must be modified to support public OAuth clients that can use the authorization code flow

Endpoint

Request Body

Response Body

Notes/Modifications

New Service?

POST /oauth2/client/

OAuthClient

OAuthClient

OAuthClient extended to require a new field clientType (enum with values PUBLIC,CONFIDENTIAL)

POST /oauth2/client/secret/{id}

None

OAuthClientIdAndSecret

Do not generate client secrets for public clients.

POST /oauth2/consent

OIDCAuthorizationRequest

OAuthAuthorizationResponse

OIDCAuthorizationRequest extended to include code_challenge, code_challenge_method. The fieldcode_challenge is required for clients of type PUBLIC.

POST /oauth2/token

Multiple parameters

OIDCTokenResponse

Additional request parameter code_verifier, required if the corresponding authorization code is associated with a code_challenge.

POST /oauth2/token

Multiple parameters

OIDCTokenResponse

Client secret not required if the client ID is a public client.

POST /oauth2/token

Multiple parameters

OIDCTokenResponse

If a public client uses an expired/rotated refresh token, revoke the current ‘version’ of the refresh token

(e.g. refresh token is abcd with ID: 7, when it is used, the new refresh token is efgh with ID: 7. if someone attempts to use abcd again, revoke efgh.

POST /oauth2/consent

POST /oauth2/token

-

-

Restrict client 0 from using these endpoints (this is already the case, but it is unclear if it is explicit).

Support for first-party command line public clients

Command line apps (like the Synapse Python client) cannot always support the authorization code flow because some environments do not have a browser. To enable this, we must do at least one of the two:

  • Support another OAuth grant type (e.g. device_code)

  • Add a user-authenticated service to generate an OAuth refresh token

A user-authenticated service is similar to the OAuth token generation service in GitHub, and is similiar to how users retrieve API keys today, so we will move forward with that.

Endpoint

Request Body

Response Body

Notes

New Service?

N/A

N/A

N/A

Add a bootstrapped ‘Synapse Command Line’ OAuth client.

We use a new client and not client 0 for two reasons

  • If we add OAuth flows to command line apps, a user should see a prompt for authorizing ‘Synapse Command Line’, which they wouldn’t see for client 0

  • There is a limit on active refresh tokens per user-client combination. If we decide to use client 0 to issue refresh tokens elsewhere (e.g. for browser sessions), then they could interfere/invalidate command line sessions.

POST /oauth2/userGeneratedToken

OIDCTokenGenerationRequest {

name: string (a unique-to-the-user, human-readable name. if unspecified, this will be a UUID)

clientId: string (the client that can use the OAuth token)

scope: Array<OAuthScope> (scopes granted by the tokens)

claims: OIDCClaimsRequest (claims granted by the tokens)

}

OIDCTokenResponse

Generates a token response that users can copy and paste into the command line client. Effectively replaces API keys.

In most cases, the clientId will be set to the ID used for the bootstrapped command line client (SWC could even hard-code it, since users likely only need to generate tokens for this client).

...

References

[1] OAuth 2.0 (RFC 6749)

...