Skip to end of banner
Go to start of banner

2FA (MFA) Support

Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

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”.

From PLFM-5411 - Getting issue details... STATUS MFA support in Synapse becomes a requirement.

For more information about MFA please refer to the OWASP cheat sheet on MFA: https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html .

While there are many different types of factors that can used as evidence when authenticating a user (certifcates, hardware tokens, email, sms etc) the long story short is that the option that is the most commonly adopted and simplest without compromising security is to implement MFA using time based one-time passwords (TOPT) as the second factor to authenticate a user.

TOPT builds on top of https://en.wikipedia.org/wiki/One-time_password and https://en.wikipedia.org/wiki/HMAC-based_one-time_password with the idea that having a shared secret and a fixed counter based on unix time and time window you can generate a unique code that expires after the time window. This has the advantage over other methods that the OTP can be generated completely offline without having to transmit the code to the user in potentially insecure manners (e.g. email, sms, push notifications).

Note that while there exists 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 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.

MFA vs 2FA

MFA simply means that in order to authenticate the user needs to provide at least two factors (e.g. password plus OTP), 2FA is a form of MFA where only 2 factors are always needed. Generally, most providers (google, facebook, github, reddit etc) implement 2FA simply because the added security of a second factor is enough and the system is much more user friendly.

When to ask/enforce 2FA?

The short answer is to prompt the user for a second factor when the user authenticates, basically we can ask the second factor during login.

PATs and (deprecated) API keys for programmatic access

Synapse supports programmatic access through the use of personal access tokens. In general PATs or other machine-based flows do not ask for 2FA at any point simply because they are considered authorization tokens (and additionally a human might not be present to input the second factor).

We should instead enforce 2FA to be enabled for any operation that allow the user to generate machine tokens (e.g. API keys, PATs, adding oauth clients etc).

Admin Accounts

Synapse also have administrative accounts that have almost unlimited access to Synapse, we should enforce 2FA to be enabled for such accounts.

OAuth Providers

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

While there exist a standard to ask the provider to enforce mfa (See acr_values parameter in the authentication request: (https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) that would include in the claims of the (OIDC) id token the amr and acr values, that indicates if the user performed MFA (See https://www.rfc-editor.org/rfc/rfc8176.html ), it does not seem to be implemented by providers (e.g. see Google https://accounts.google.com/.well-known/openid-configuration, our supported OAuth provider).

We could try enforcing the prompt for 2FA for Synapse users when they are redirected back to Synapse to validate the auth code and obtain an access token.

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.

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.

Note that we do implement refresh tokens, meaning that the 3rd party can automatically refresh the token without user interaction:

Refresh tokens issued by Synapse are single-use only, and expire if unused for 180 days. Using the refresh_token grant type will cause Synapse to issue a new refresh token in the token response, and the old refresh token will become invalid.

Generally, refresh tokens are not affected by 2FA, for example google only invalidates the refresh token in some cases if the user updates the password, if the 2FA is required by the organization and the user does not have 2FA enabled then the request for a refresh token fails with a “2FA enrollment required” error.

Docker Client

Synapse implements authentication for docker clients in order to use the Synapse docker registry. The synapse credentials are 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 the request will fail. 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).

We would need to setup a plan for informing users using user/password with the docker clients that its support will be dismissed after x days.

2FA Prompt Enforcement

It is clear that at the least we need to ask for the 2FA when the user authenticates using username/password and when a user login through an external OAuth provider.

Additionally, we might consider adding an expiration time to a valid 2FA authentication after which the user needs to be prompted for 2FA to address refresh tokens potentially bypassing 2FA.

Note: currently it is not possible to obtain a refresh token from an access token issued by the login through username/password or OAuth providers, so this does not affect users that authenticate from synapse.org (their tokens always expire after 24 hours).

There is no existing standard for integrating 2FA when authenticating, each provider seems to implement a slightly different approach (Note that the following are not necessarily internal API implementations):

  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.

Proposed 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:

  1. The server to generate a shared secret and send it to the user

  2. Verify that the user added the shared secret correctly to their TOTP application and enable 2FA.

  3. Generate a set of on-time use recovery codes so that the user can regain access to their account if they lost their device.

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

{
"type": "totp",
"password": "<user_password>"
}
{
"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

{
"secretId": <id from enroll API>,
"totp": <totp from app>
}
{
  "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 already enabled if the request is successful.

POST /2fa/recovery_codes

{
  "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 authentication. 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.

For example, first we send a login request:

curl -X POST --url https://repo-prod.prod.sagebase.org/auth/v1/login -H "Content-Type: application/json" -d '{"username":"user", "password":"password"}'

We receive the 2fa_required error:

HTTP/2 401

{"concreteType":"org.sagebionetworks.repo.model.ErrorResponse","reason":"2FA is required to authenticate", "error_code": "2fa_required", "2fa_token": "c3VwZXIgczNjcjN0IHRva2Vu"}

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

curl -X POST --url https://repo-prod.prod.sagebase.org/auth/v1/oauth2/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:

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}

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

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?

  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?

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

  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.

  5. Should we send an email when we enable/disable 2FA with a link to the documentation?

  • No labels