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. 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.
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
Add field to label OAuth 2 clients as either “public” or “confidential”; backfill existing clients as “confidential”
Public clients should not be issued secrets
Public clients should be required to use PKCE
Revoke the current iteration of a refresh token when an old refresh token is used
Don’t save consent records for public OAuth clients
To fully support Synapse command line clients, in addition to the above, we must:
Add a bootstrapped OAuth client for Synapse command line apps
Add service to issue a (user-authenticated) refresh token for the Synapse command line apps OAuth client
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
...
1
...
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. 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. Additionally, this will hasten the deprecation of API keys.
To securely support OAuth 2 public clients, these actions are needed:
Support PKCE in the authorization code flow:
Jira Legacy server System JIRA columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-6337 Add field to label OAuth 2 clients as either “public” or “confidential”; backfill existing clients as “confidential”:
Jira Legacy server System JIRA columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-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 expired/used refresh token is used:
Jira Legacy server System JIRA columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-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 server System JIRA columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-6340 Add service to issue a (user-authenticated) refresh token for the Synapse command line apps OAuth client:
Jira Legacy server System JIRA columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-6341
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 | 1 | Access: Unlimited Refresh: 100 per user per OAuth client |
Stored as/Re-retrievable | Unhashed/Yes | Unhashed/Yes | Access: Not stored/No Refresh: Hashed token/No |
...
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.PKCE would require clients to generate a code and send a hash of the code to Synapse when initiating the authorization code OAuth flow. When redeeming the authorization code, the client must send the original code. Synapse would then hash the code and verify that the hash matches the initial hash sent by the client. of an OAuth client’s credentials to execute the attack.
For more details, expand the following section:
Expand | ||
---|---|---|
| ||
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. |
...
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:
...
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 this the missing behavior.
Token binding is also more complicatedcomplicated, so we won’t use it. For details, expand the following section.
Expand | ||
---|---|---|
| ||
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). |
Risk in Saving Consent Records for Public OAuth Clients
In Synapse, we keep track of when a user consents to a particular selection of scope and claims, so that a user does not have to re-authorize an application that has already been authorized. This is for convenience; this mechanism isn’t specification-related.
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.
For an illustration of this scenario, expand the following section:
Expand | ||
---|---|---|
| ||
Consider
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?
...
...
...
...
OAuthClient extended to require a new field clientType
(enum with values PUBLIC
,CONFIDENTIAL
)
...
❌
...
POST /oauth2/client/secret/{id}
...
None
...
...
Do not generate client secrets for public clients.
...
❌
...
...
...
...
OIDCAuthorizationRequest extended to include code_challenge
, code_challenge_method
. The fieldcode_challenge
is required for clients of type PUBLIC
.
...
❌
...
...
Multiple parameters
...
...
Additional request parameter code_verifier
, required if the corresponding authorization code is associated with a code_challenge
.
...
❌
...
...
Multiple parameters
...
...
Client secret not required if the client ID is a public client.
...
❌
...
...
Multiple parameters
...
...
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 abcd
is used again, revoke efgh
.
...
❌
...
...
-
...
-
...
Restrict client 0 from using these endpoints.
...
❌
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
...
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). |
Risk in Saving Consent Records for Public OAuth Clients
In Synapse, we keep track of when a user consents to a particular selection of scope and claims, so that a user does not have to re-authorize an application that has already been authorized. This is for convenience; this mechanism isn’t specification-related.
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.
For an illustration of this scenario, expand the following section:
Expand | ||
---|---|---|
| ||
Consider
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? |
---|---|---|---|---|
OAuthClient extended to require a new field | ❌ | |||
None | Do not generate client secrets for public clients. | ❌ | ||
OIDCAuthorizationRequest extended to include | ❌ | |||
Multiple parameters | Additional request parameter | ❌ | ||
Multiple parameters | Client secret not required if the client ID is a public client. | ❌ | ||
Multiple parameters | If a public client uses an expired/rotated refresh token, revoke the current ‘version’ of the refresh token (e.g. refresh token is | ❌ | ||
- | - | 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.
|
|
|
|
|
---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
...
References
...