Versions Compared

Key

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

...

Note that while there exists a standard for the TOPT implementation (https://datatracker.ietf.org/doc/html/rfc6238 ) there is no standard for its integration in application flows, but the idea is generally simple:

  1. We need a way to enroll users in MFA, e.g. enable a way to add a second factor shared secret (used to generate TOTPs) to the user account

  2. We need a way to prompt the user for the second factor in sensitive flows such as login, updating the user profile, adding PATs etc.

...

Synapse also supports an OIDC provider (currently only google) during authentication, this means that the user is redirected to the provider to perform authentication and redirected back to synapse once that is successful. The most common approach is to not ask or even let the user enable 2FA in the relying system when authentication is performed through a 3rd party, since 2FA would potentially be performed by the authentication provider itself (in my tests slack is the only one that asked for 2FA when authenticating through google).

...

However, the auth code that is generated by the OAuth provider is usually valid for a brief amount of time (e.g. 30-60 seconds, Synapse codes are valid for 60 seconds) so we need to design a protocol that allows to consume the code and only then prompt the user for 2FA. This also means that this request cannot be “resubmitted” to include the 2FA code and we need a separate endpoint to finalize 2FA.

OAuth Clients

Synapse supports registering external OAuth clients so that external application can authenticate synapse users: we become the authentication provider. When the user logs in (e.g. before consenting) we follow the 2FA flow for the user account.

...

Synapse implements authentication for docker clients in order to use the Synapse docker registry. The synapse credentials are or a PAT is passed through basic auth and are validated by the backend.

In this case users should really use PATs for programmatic access, if they do provide user/password or an access token (of type != PAT) instead, 2FA will be required and and 2FA is enabled, the request will fail to authenticate the user. There is not fallback here since credentials are required for every request and the user would need to provide the OTP for each operation every x seconds (validity of the OTP).

...

API

Request

Response

Description

POST /2fa/enroll

Code Block
{
"type": "totp",
"password":
"<user_password>"
}
Code Block
{
"id": <id of the generated secret>,
"type": "totp",
"secret": "<Base64 encoded secret>",
"secretBase32": "<Base32 encoded version of the secret>"
"alg": "SHA1",
"digits": 6,
"period": 30
}

Initiate the enrollment for the user to 2FA.

The server generates a shared secret to use with an OTP application that is sent back to the user.

The client can generate a QR code for convenience so that the user can scan the secret when adding it to the OTP application (e.g. google authenticator). The URL to embed in the QR code can follow this format: https://github.com/google/google-authenticator/wiki/Key-Uri-Format#issuer .

For example: otpauth://totp/Synapse:alice@google.com?secret=<secretBase32>&issuer=Synapse%20Prod&algorithm=SHA1&digits=6&period=30.

Note that the endpoint can be invoked even if the user has 2FA already enabled.

The server will re-generate a secret that is not used until it is enabled. This , this allows the user to reset the 2FA without affecting existing 2FA.

POST /2fa

Code Block
{
"secretId": <id from enroll API>,
"totp": <totp from app>
}
Code Block
{
  "status": "enabled"
}

Enable 2FA for the user, uses the secret that was generated from the enroll API.

Note that the server will use only one secret to verify TOTPs.

This backend will replace other 2FA secrets already enabled if the request is successful.

POST /2fa/recovery_codes

Code Block
languagejson
{
  "codes": ["abc", "efg"]
}

Re-generates a set of recovery codes (we can provide at least 10 codes).

The codes are a one-time use code that the user can use to recover access to their account.

This endpoint will re-generate the codes on each call and associate them with the currently enabled 2FA.

The generated codes are hashed and never retrieved again.

Note that the user can end up using all of the recovery codes, in this case we should warn the user (maybe an email, or a notification in the browser?) that new codes should be re-generated.

If 2FA is not enabled the API will return a 403 with the error code 2fa_enrollement_required.

DELETE /2fa

Disable 2FA for the user.

If 2FA is not enabled the API will return a 403 with the error code 2fa_enrollement_required

Proposed 2FA auth API Design

While there is no clear standard to implement 2FA on top of OAuth2 or OIDC, we can make use of the OAuth 2 grant extension framework to integrate 2FA (See https://www.rfc-editor.org/rfc/rfc6749#section-4.5 ). The inspiration is taken from auth0 implementation (https://auth0.com/docs/secure/multi-factor-authentication/authenticate-using-ropg-flow-with-mfa ).

The proposal is to extend the existing /oauth2/token endpoint to support a new grant_type http://synapse.org/oauth/grant-type/2fa-otp to obtain an access token when 2FA is enabled to finalize authenticationThe proposal is to add a new API endpoint that allows the user to obtain an access code in exchange from a server generated code and a totp code. The reasoning behind having a separate endpoint is that since we want to ask for the 2FA when the authentication is done through Google we cannot “resubmit” the request with the totp code (since the code is short lived).

The user can authenticate through the /login or the /oauth2/session2 endpoints. In the latter case the endpoint is used when an OAuth provider (google) redirects back to synapse.

If the credentials are valid but the 2FA is enabled, instead of issuing a new access token the APIs should return a 401, with the mfa_required error code and a 2fa_token property. This token is opaque to the client and can encode the context (such as the scope of the original indented access token).

With the new grant http://synapse.org/oauth/grant-type/2fa-otp grant_type a request to the /oauth2/token endpoint can be submitted to finalize authentication with the following parameters:

  • client_id: the only value that would make sense is the synapse client id (which is 0), so it can be omitted.

  • 2fa_token: The token received from the 401 when authenticating

  • otp_type: The type of otp that the user used, one of [totp, recovery_code]

  • otp_code: The otp code according to the type (e.g. the otp generated by google authenticator in case of totp)

If successful, the response will include the access token.

The 2fa_token and a totp code can be used to finalize authentication with 2fa:

API

Request

Response

Description

/2fa/token

Code Block
languagejson
{
 "2fa_token": <token received in the 401 mfa_required response>,
 "otp_type": "totp",
 "otp_code": <totp from app>
}
Code Block
{
 "access_token": "<the access token that would be obtain from authentication>"
}

Allows to obtain an access token in exchange from the 2fa_token received from a 401 with error_code=mfa_required and the totp that the user inputs.

For example, first we send a login request:

...

Then we ask the user for the totp and we send a request to the token endpoint to finalize authentication:

Code Block
curl -X POST --url https://repo-prod.prod.sagebase.org/auth/v1/oauth22fa/token -H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=http://synapse.org/oauth/grant-type/2fa-otp \
-d 2fa_token=c3VwZXIgczNjcjN0IHRva2Vu \
-d otp_type=totp
-d otp_code=124667

Now the response will include the access token:

Code Block
HTTP/2 200

{"access_token": "c3VwZXIgczNjcjN0IGFjY2VzcyB0b2tlbg=="}

Additionally, the following endpoints will require the user to have 2FA enabled:

  1. POST /personalAccessToken

  2. GET /personalAccessToken

  3. GET /personalAccessToken/{id}

  4. DELETE /personalAccessToken/{id}

...

d otp_code=124667

Now the response will include the access token:

Code Block
HTTP/2 200

{"access_token": "c3VwZXIgczNjcjN0IGFjY2VzcyB0b2tlbg=="}

Open Questions

  1. Should we implement some sort of step-up authentication? E.g. when the user tries to create a PAT, should we ask for the 2FA code? Or is it enough to validate that the user has 2FA enabled?enabled?
    → We decided to avoid adding additional checks, we need to inform the user when PAT are added/removed with an email.

  2. The design requires that the 2FA/token takes in input a 2FA_token. This token would help the backend to understand the context (e.g. login, scopes). This is needed because Synapse does not use sessions and we need to support google as a 3rd party. How long should this token be valid? Should it be a one-time use token?
    → The token should only be valid for a brief time, e.g. 5 minutes.

  3. I specifically didn’t add any validation for access tokens. This means that even after enrolling in 2FA existing tokens will be valid. Should we instead encode into the access tokens a claim such as “2fa_auth_time” that we can validate (for presence only) when the user has 2FA enabled? This would mean that any token after 2FA is enabled will be invalidated and the user needs to login again (potentially having to input the 2FA code twice).code twice).
    → This is still open for discussion: on one hand invalidating previous issued tokens is more secure, on the other hand it has the potential to break existing workflows and script and reducing usuability

  4. Relevant to the previous point: using the refresh token grant we can technically keep refreshing tokens without user interaction (at least through an oauth client), should we limit the amount of time before the user needs to re-authenticate? E.g. Using the previous 2fa_auth_time we could check if 2fa is enabled and if it was performed in the last 30 days.Should we send an email when we enable/disable 2FA with a link to the documentation?days.
    → We decided that client token issued through client credentials are secure enough and there is no need for additional 2FA checking

  5. Should we send an email when we enable/disable 2FA with a link to the documentation?
    → Open JIRA(s) related to this.

  6. Should we implement the standard to support for the acr_values in the /oauth2/token endpoint to allow oauth clients to enforce the use of 2FA? (See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest )