2FA (MFA) Support
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”.
From - PLFM-5411Getting 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 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:
We need a way to enroll users in MFA, e.g. enable a way to add a shared secret (used to generate TOTPs) to the user account
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 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).
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. 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.
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 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 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).
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):
AWS, Reddit, Last Pass: Get a 200, response ask for mfa, resubmit the request with the otp (no need for session state)
Github: Get a 302 and submit code to a second page (state session based)
Jumpcloud: Get a 401, response ask for mfa, submit the otp to a second API (state session based)
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.
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.
Twitter: Single API for each operation that changes the request/response based on what the client needs to do.
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:
The server to generate a shared secret and send it to the user
Verify that the user added the shared secret correctly to their TOTP application and enable 2FA.
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 |
---|---|---|---|
{
"type": "totp"
} | {
"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: 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. | |
{
"secretId": <id from enroll API>,
"totp": <totp from app>
} | 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. | ||
| 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 400. | ||
|
| Disable 2FA for the user. If 2FA is not enabled the API will return a 400. | |
| 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 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).
The 2fa_token and a totp code can be used to finalize authentication with 2fa:
API | Request | Response | Description |
---|---|---|---|
/2fa/token | Allows to obtain an access token in exchange from the 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:
We receive the 2fa_required error:
Then we ask the user for the totp and we send a request to the token endpoint to finalize authentication:
Now the response will include the access token:
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 |
---|---|---|---|
None | Allows the user to request a twoFaResetToken that is sent by email, can be requested with a twoFaToken returned by an authentication attempt. | ||
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
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.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.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 usability → - PLFM-7637Getting issue details... STATUSRelevant 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 checkingShould we send an email when we enable/disable 2FA with a link to the documentation?
→ Open JIRA(s) related to this.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 ) → - PLFM-7629Getting issue details... STATUS
What other ways should we support as a backup to regain access to the account (aside from recovery codes)? Email, sms, security keys? → - PLFM-7633Getting issue details... STATUS