Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Table of Contents
minLevel1
maxLevel7

Introduction

According to OWASP identification and authentication failures are still in their top 10 list, additionally according to a Microsoft study “your account is more than 99.9% less likely to be compromised if you use MFA”.

...

  1. AWS, Reddit, Last Pass: Get a 200, response ask for mfa, resubmit the request with the otp (no need for session state)

  2. Github: Get a 302 and submit code to a second page (state session based)

  3. Jumpcloud: Get a 401, response ask for mfa, submit the otp to a second API (state session based)

  4. Paycheck Flex: This is a weird one, before submitting any credentials the user is asked to enter the code (from sms or email) that is submitted together with the credentials. The mfa requirement for any username is public.

  5. Google, Microsoft: Send push notification to the device if the TOPT where it asks to confirm the login attempt, there is a challenge endpoint that is polled every second to check the user answer. Otherwise, the user can input the code that is sent to the challenge API.

  6. Twitter: Single API for each operation that changes the request/response based on what the client needs to do.

  7. Auth0 API (See https://auth0.com/docs/secure/multi-factor-authentication/authenticate-using-ropg-flow-with-mfa ): This is the most interesting, Auth0 provides developers API to handle user identity. They implement an API to handle mfa on top of the standard oauth2 token endpoint. For example, when requesting a password grant an error mfa_required, with a mfa_token is returned if the user has 2FA enabled. A challenge API is used to decide which factor to use, and finally a new request to the token endpoint with a special grant of type http://auth0.com/oauth/grant-type/mfa-otp is sent with the mfa_token and the otp code to receive an access token.

...

2FA Enrollment API Design

In its most basic form in order for the user to enable 2FA using TOTP on their account we need a set of APIs that allow:

...

Note that it is important to give the user at least one way to recover access without the OTP application. We can later extend this using for example SMS or Email as a backup to send the user otp codes to regain access. We would also need to setup a way for the user to contact us in case any attempt to regain access is failing so that we can potentially help them regain access (e.g. disable 2FA for them once their identify is verified).

API

Request

Response

Description

POST /2fa/enroll

Code Block
languagejson
{
"type": "totp"
}
Code Block
languagejson
{
"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 allows the user to reset the 2FA without affecting existing 2FA.

POST /2fa

Code Block
languagejson
{
"secretId": <id from enroll API>,
"totp": <totp from app>
}
Code Block
languagejson
{
  "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

400.

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

...

400.

GET /2fa

Code Block
languagejson
{
  "status": "enabled"
}

Allows to fetch the current status of 2fa for the user.

2FA auth API Design

The 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 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
languagejson
{
 "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.

The otp_type parameter can be one of [totp, recovery_codes], the value of the otp_code is treated as the generated totp or a recovery code according to the type.

For example, first we send a login request:

...

Code Block
HTTP/2 200

{"access_token": "c3VwZXIgczNjcjN0IGFjY2VzcyB0b2tlbg=="}

Open Questions

...

2FA Reset Workflow (Added April 2024)

To aid the user in regaining access to their account when the user cannot access their TOTP device neither their recovery codes, we added a workflow that allows to automatically disable 2fa through the validation of the user primary factor (credentials) and access to email. The workflow introduces two new APIs:

API

Request

Response

Description

/2fa/reset

Code Block
languagejson
{
 "userId": <user id>,
 "twoFaResetEndpoint": "<portal endpoint prefix for the email link>,
 "twoFaToken": <token received in the 401 mfa_required response, authentication only>
}

None

Allows the user to request a twoFaResetToken that is sent by email, can be requested with a twoFaToken returned by an authentication attempt.

/2fa/disable

Code Block
languagejson
{
 "twoFaToken": <token received in the 401 mfa_required response, authentication only>,
 "twoFaResetToken": <decoded token received by email through the /2fa/reset API call>
}

None

Allows the user to disable their 2fa, the twoFaResetToken is the decoded token received by email from the /2fa/reset API call. This token allows to verify access to the user email. The other parameter, twoFaToken is the usual token received when authenticating with 2fa enabled, this token allows to verify the primary factor (credentials).

Given that the user can disable 2fa through email validation, we also added the 2fa enforcement when the user changes their password since a password can be reset through email. The API for changing the password (either through using the current password or through a reset email link) was modified to return a 401 with twoFaToken when 2fa is enabled.

The change password API now accepts a new type, https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/auth/ChangePasswordWithTwoFactorAuthToken.html that allows to change the password with the twoFaToken received in the response.

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?
    → 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).
    → 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 usuabilityworkflows and script and reducing usability →

    Jira Legacy
    serverSystem JIRA
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-7637

  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.
    → 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 )

    Jira Legacy
    serverSystem JIRA
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-7629

  7. What other ways should we support as a backup to regain access to the account (aside from recovery codes)? Email, sms, security keys?

    Jira Legacy
    serverSystem JIRA
    serverIdba6fb084-9827-3160-8067-8ac7470f78b2
    keyPLFM-7633