20 KiB
Overview of Authentication at Bitwarden
Table of Contents
Authentication Methods
Bitwarden provides 5 methods for logging in to Bitwarden, as defined in our AuthenticationType enum. They are:
- Login with Master Password
- Login with Auth Request (aka Login with Device) — authenticate with a one-time access code
- Login with Single Sign-On — authenticate with an SSO Identity Provider (IdP) through SAML or OpenID Connect (OIDC)
- Login with Passkey (WebAuthn)
- Login with User API Key — authenticate with an API key and secret
Login Initiation
Angular Clients - Initiating Components
A user begins the login process by entering their email on the /login screen (LoginComponent). From there, the user must click one of the following buttons to initiate a login method by navigating to that method's associated "initiating component":
"Continue"→ user stays on theLoginComponentand enters a Master Password"Log in with device"→ navigates user toLoginViaAuthRequestComponent"Use single sign-on"→ navigates user toSsoComponent"Log in with passkey"→ navigates user toLoginViaWebAuthnComponent- Note: Login with Passkey is currently not available on the Desktop client.
Note
- Our Angular clients do not support the Login with User API Key method.
- The Login with Master Password method is also used by the
RegistrationFinishComponentandCompleteTrialInitiationComponent(the user automatically gets logged in with their Master Password after registration), as well as theRecoverTwoFactorComponent(the user logs in with their Master Password along with their 2FA recovery code).
CLI Client - LoginCommand
The CLI client supports the following login methods via the LoginCommand:
- Login with Master Password
- Login with Single Sign-On
- Login with User API Key (which can only be initiated from the CLI client)
Important
While each authentication method has its own unique logic, this document discusses the logic that is generally common to all authentication methods. It provides a high-level overview of authentication and as such will involve some abstraction and generalization.
The Login Credentials Object
When the user presses the "submit" action on an initiating component (or via LoginCommand for CLI), we build a login credentials object, which contains the core credentials needed to initiate the specific login method.
For example, when the user clicks "Log in with master password" on the LoginComponent, we build a PasswordLoginCredentials object, which is defined as:
export class PasswordLoginCredentials {
readonly type = AuthenticationType.Password;
constructor(
public email: string,
public masterPassword: string,
public twoFactor?: TokenTwoFactorRequest,
public masterPasswordPoliciesFromOrgInvite?: MasterPasswordPolicyOptions,
) {}
}
Notice that the type is automatically set to AuthenticationType.Password, and the PasswordLoginCredentials object simply requires an email and masterPassword to initiate the login method.
Each authentication method builds its own type of credentials object. These are defined in login-credentials.ts.
PasswordLoginCredentialsAuthRequestLoginCredentialsSsoLoginCredentialsWebAuthnLoginCredentialsUserApiLoginCredentials
After building the credentials object, we then call the logIn() method on the LoginStrategyService, passing in the credentials object as an argument: LoginStrategyService.logIn(credentials)
The LoginStrategyService and our Login Strategies
The LoginStrategyService acts as an orchestrator that determines which of our specific login strategies should be initialized and used for the login process.
Important
Our authentication methods are handled by different login strategies, making use of the Strategy Design Pattern. Those strategies are:
PasswordLoginStrategyAuthRequestLoginStrategySsoLoginStrategyWebAuthnLoginStrategyUserApiLoginStrategyEach of those strategies extend the base
LoginStrategy, which houses common login logic.
More specifically, within its logIn() method, the LoginStrategyService uses the type property on the credentials object to determine which specific login strategy to initialize.
For example, the PasswordLoginCredentials object has type of AuthenticationType.Password. This tells the LoginStrategyService to initialize and use the PasswordLoginStrategy for the login process.
Once the LoginStrategyService initializes the appropriate strategy, it then calls the logIn() method defined on that particular strategy, passing on the credentials object as an argument. For example: PasswordLoginStrategy.logIn(credentials)
To summarize everything so far:
Initiating Component (Submit Action) # ex: LoginComponent.submit()
|
Build credentials object # ex: PasswordLoginCredentials
|
Call LoginStrategyService.logIn(credentials)
|
Initialize specific strategy # ex: PasswordLoginStrategy
|
Call strategy.logIn(credentials) # ex: PasswordLoginStrategy.logIn(credentials)
...
The logIn() and startLogIn() Methods
Each login strategy has its own unique implementation of the logIn() method, but each logIn() method performs the following general logic with the help of the credentials object:
- Build a
LoginStrategyDataobject with aTokenRequestproperty - Cache the
LoginStrategyDataobject - Call the
startLogIn()method on the baseLoginStrategy
Here are those steps in more detail:
-
Build a
LoginStrategyDataobject with aTokenRequestpropertyEach strategy uses the credentials object to help build a type of
LoginStrategyDataobject, which contains the data needed throughout the lifetime of the particular strategy, and must, at minimum, contain atokenRequestproperty (more on this below).export abstract class LoginStrategyData { tokenRequest: | PasswordTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest | UserApiTokenRequest | undefined; abstract userEnteredEmail?: string; }Each strategy has its own class that implements the
LoginStrategyDatainterface:PasswordLoginStrategyDataAuthRequestLoginStrategyDataSsoLoginStrategyDataWebAuthnLoginStrategyDataUserApiLoginStrategyData
So in our ongoing example that uses the "Login with Master Password" method, the call to
PasswordLoginStrategy.logIn(PasswordLoginCredentials)would build aPasswordLoginStrategyDataobject that contains the data needed throughout the lifetime of thePasswordLoginStrategy.That
PasswordLoginStrategyDataobject is defined as:export class PasswordLoginStrategyData implements LoginStrategyData { tokenRequest: PasswordTokenRequest; userEnteredEmail: string; localMasterKeyHash: string; masterKey: MasterKey; forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None; }Each of the
LoginStrategyDatatypes have varying properties, but one property common to all is thetokenRequestproperty.The
tokenRequestproperty holds some type ofTokenRequestobject based on the strategy:PasswordTokenRequest— used by bothPasswordLoginStrategyandAuthRequestLoginStrategySsoTokenRequestWebAuthnLoginTokenRequestUserApiTokenRequest
This
TokenRequestobject is also built within thelogIn()method and gets added to theLoginStrategyDataobject as thetokenRequestproperty.
-
Cache the
LoginStrategyDataobjectBecause a login attempt could "fail" due to a need for Two Factor Authentication (2FA) or New Device Verification (NDV), we need to preserve the
LoginStrategyDataso that we can re-use it later when the user provides their 2FA or NDV token. This way, the user does not need to completely re-enter all of their credentials.The way we cache this
LoginStrategyDatais simply by saving it to a property calledcacheon the strategy. There will be more details on how this cache is used later on.
-
Call the
startLogIn()method on the baseLoginStrategyNext, we call the
startLogIn()method, which exists on the baseLoginStrategyand is therefore common to all login strategies. ThestartLogIn()method does the following:-
Makes a
POSTrequest to the/connect/tokenendpoint on our Identity Server-
REQUESTThe exact payload for this request is determined by the
TokenRequestobject. More specifically, the baseTokenRequestclass contains atoIdentityToken()method which gets overridden/extended by the sub-classes (PasswordTokenRequest.toIdentityToken(), etc.). ThistoIdentityToken()method produces the exact payload that gets sent to our/connect/tokenendpoint.The payload includes OAuth2 parameters, such as
scope,client_id, andgrant_type, as well as any other credentials that the server needs to complete validation for the specific authentication method. -
RESPONSEThe Identity Server validates the request and then generates some type of
IdentityResponse, which can be one of three types:-
- Meaning: the user has been authenticated
- Response Contains:
- Authentication information, such as:
- An access token (which is a JWT with claims about the user)
- A refresh token
- Decryption information, such as:
- The user's master-key-encrypted user key (if the user has a master password), along with their KDF settings
- The user's user-key-encrypted private key
- A
userDecryptionOptionsobject that contains information about which decryption options the user has available to them - A flag that indicates if the user is required to set or change their master password
- Any master password policies the user is required to adhere to
- Authentication information, such as:
-
- Meaning: the user needs to complete Two Factor Authentication
- Response Contains:
- A list of which 2FA providers the user has configured
- Any master password policies the user is required to adhere to
-
IdentityDeviceVerificationResponse- Meaning: the user needs to verify their new device via new device verification
- Response Contains: a simple boolean property that states whether or not the device has been verified
-
-
-
Calls one of the
process[IdentityType]Response()methodsEach of these methods builds and returns an
AuthResultobject, which gets used later to determine how to direct the user after an authentication attempt.The specific method that gets called depends on the type of the
IdentityResponse:-
If
IdentityTokenResponse→ callprocessTokenResponse()- Instantiates a new
AuthResultobject - Calls
saveAccountInformation()to initialize the account with information from theIdentityTokenResponse- Decodes the access token (a JWT) to get information about the user (userId, email, etc.)
- Sets several things to state:
- The account (via
AccountService) - The user's environment
userDecryptionOptionsmasterPasswordUnlockData(ifuserDecryptionOptionsallows for master password unlock):- Salt
- KDF config
- Master-key-encrypted user key
- Access token and refresh token
- KDF config
- Premium status
- The account (via
- If the
IdentityTokenResponsecontains atwoFactorToken(because the user previously selected "remember me" for their 2FA method), set that token to state - Sets cryptographic properties to state: master key, user key, private key
- Sets a
forceSetPasswordReasonto state (if necessary) - Returns the
AuthResult
- Instantiates a new
-
If
IdentityTwoFactorResponse→ callprocessTwoFactorResponse()- Instantiates a new
AuthResultobject - Sets
AuthResult.twoFactorProvidersto the list of 2FA providers from theIdentityTwoFactorResponse - Sets that same list of of 2FA providers to global state (memory)
- Returns the
AuthResult
- Instantiates a new
-
If
IdentityDeviceVerificationResponse→ callprocessDeviceVerificationResponse()- Instantiates a new
AuthResultobject - Sets
AuthResult.requiresDeviceVerificationtotrue - Returns the
AuthResult
- Instantiates a new
-
-
Handling the AuthResult
The AuthResult object that gets returned from the process[IdentityType]Response() method ultimately gets returned up through the chain of callers until it makes its way back to the initiating component (ex: the LoginComponent for Login with Master Password).
The initiating component will then use the information on that AuthResult to determine how to direct the user after an authentication attempt.
Below is a high-level overview of how the AuthResult is handled, but note again that there are abstractions in this diagram — it doesn't depict every edge case, and is just meant to give a general picture.
Initiating Component (Submit Action) < - - -
| \
LoginStrategyService.logIn() - \
| \ # AuthResult bubbles back up
strategy.logIn() - \ # through chain of callers
| \ # to the initiating component
startLogIn() - \
| \
process[IdentityType]Response() - \
| \
returns AuthResult - - - - - - - -
|
- - - - - - - - - - # Initiating component then
| # uses the AuthResult in
handleAuthResult(authResult) # handleAuthResult()
|
IF AuthResult.requiresTwoFactor
| # route user to /2fa to complete 2FA
|
IF AuthResult.requiresDeviceVerification
| # route user to /device-verification to complete NDV
|
# Otherwise, route user to /vault
Now for a more detailed breakdown of how the AuthResult is handled...
There are two broad types of scenarios that the user will fall into:
- Re-submit scenarios
- Successful Authentication scenarios
Re-submit Scenarios
There are two cases where a user is required to provide additional information before they can be authenticated: Two Factor Authentication (2FA) and New Device Verification (NDV). In these scenarios, we actually need the user to "re-submit" their original request, along with their added 2FA or NDV token. But remember earlier that we cached the LoginStrategyData. This makes it so the user does not need to re-enter their original credentials. Instead, the user simply provides their 2FA or NDV token, we add it to their original (cached) LoginStrategyData, and then we re-submit the request.
Here is how these scenarios work:
User must complete Two Factor Authentication
- Remember that when the server response is
IdentityTwoFactorResponse, we set 2FA provider data into state, and also setrequiresTwoFactortotrueon theAuthResult. - When
AuthResult.requiresTwoFactoristrue, the specific login strategy exports itsLoginStrategyDatato theLoginStrategyService, where it gets stored in memory. This means theLoginStrategyServicehas a cache of the original request the user sent. - We route the user to
/2fa(TwoFactorAuthComponent). - The user enters their 2FA token.
- On submission, the
LoginStrategyServicecallslogInTwoFactor()on the particular login strategy. This method then:- Takes the cached
LoginStrategyData(the user's original request), and appends the 2FA token onto theTokenRequest - Calls
startLogIn()again, this time using the updatedLoginStrategyDatathat includes the 2FA token.
- Takes the cached
User must complete New Device Verification
Note that we currently only require new device verification on Master Password logins (PasswordLoginStrategy) for users who do not have a 2FA method setup.
- Remember that when the server response is
IdentityDeviceVerificationResponse, we setrequiresDeviceVerificationtotrueon theAuthResult. - When
AuthResult.requiresDeviceVerificationistrue, the specific login strategy exports itsLoginStrategyDatato theLoginStrategyService, where it gets stored in memory. This means theLoginStrategyServicehas a cache of the original request the user sent. - We route the user to
/device-verification. - The user enters their NDV token.
- On submission, the
LoginStrategyServicecallslogInNewDeviceVerification()on the particular login strategy. This method then:- Takes the cached
LoginStrategyData(the user's original request), and appends the NDV token onto theTokenRequest. - Calls
startLogIn()again, this time using the updatedLoginStrategyDatathat includes the NDV token.
- Takes the cached
Successful Authentication Scenarios
User must change their password
A user can be successfully authenticated but still required to set/change their master password. In this case, the user gets routed to the relevant set/change password component (SetInitialPassword or ChangePassword).
User does not need to complete 2FA, NDV, or set/change their master password
In this case, the user proceeds to their /vault.
Trusted Device Encryption scenario
If the user is on an untrusted device, they get routed to /login-initiated to select a decryption option. If the user is on a trusted device, they get routed to /vault because decryption can be done automatically.
Diagram of Authentication Flows
Here is a high-level overview of what all of this looks like in the end.