Where Can I Find User MFA Sample Code?
Our User MFA endpoints make it simple to increase your application’s security by strengthening end-user authentication. To show how the integration works, we’ve created a simple JS application that uses TrueVault for user authentication and data storage. Then we implemented User MFA in a separate branch so you can see exactly what it takes to add User MFA to your existing application (see the difference here). The details may vary based on your application stack, but the flow (and of course, the TrueVault API Endpoints) will be the same.
In this screencast, Manuel walks us through the changes required to implement MFA. We’ll cover the highlights below the video.
Steps to Adding User MFA
You’ll probably want to check out the implement-user-mfa
branch of the sample app for reference.
git clone https://github.com/truevault/user-mfa-sample.git git checkout implement-user-mfa
Then, let’s look at the high-level steps required to add User MFA to an application already authenticating with TrueVault.
Enrolling
Users need to enroll in MFA to get started. This step is nothing more than a shared-secret exchange, following the TOTP Spec. The user asks TrueVault to start enrollment, get’s back a shared secret, encoded in a QR Code, and scans that with their authenticator app to save the secret. Users can pick their favorite authenticator app, as long as it follows the TOTP Spec. We recommend either Google Authenticator or Authy.
Here’s the event handler for the “Enroll” button in the sample app:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
document.getElementById("start-mfa").addEventListener('click', e => { e.preventDefault(); startUserMFAEnrollment(tvUser.access_token, tvUser.id) .then(response => { userSettingsFormEl.style.display = ''; // TrueVault gives us back a QR Code SVG that you can show // supporting the common enroll UI document.getElementById('mfa-qr-code').src = `data:image/svg+xml;base64,${btoa(response.mfa.qr_code_svg)}`; }) .catch(e => alert(e.message)); }); |
And here’s the low-level method that makes the request to TrueVault:
1 2 3 4 5 6 |
function startUserMFAEnrollment(accessToken, userId) { return tvRequest(accessToken, "POST", `users/${userId}/mfa/start_enrollment`, null, { // Tip: put your app name here, it pre-populates the name in the authenticator app issuer: 'True Do' }); } |
Then, our security-conscious user finalizes enrollment by copying two consecutive codes from their authenticator and sending them back to TrueVault to verify.
This is backed by an event handler:
1 2 3 4 5 6 7 8 9 10 11 |
userSettingsFormEl.addEventListener('submit', e => { e.preventDefault(); finalizeMFAEnrollment(tvUser.access_token, tvUser.id, this.mfa_code_1.value, this.mfa_code_2.value) .then(() => { tvUser.mfa_enrolled = true; refreshState(); }) .catch(e => alert(e.message)); }); |
And underlying request:
1 2 3 4 5 6 7 |
function finalizeMFAEnrollment(accessToken, userId, mfaCode1, mfaCode2) { return tvRequest(accessToken, "POST", `users/${userId}/mfa/finalize_enrollment`, null, { // These codes must be consecutive mfa_code_1: mfaCode1, mfa_code_2: mfaCode2 }); } |
TrueVault validates that the authenticator app was setup properly, and then ensures all future login requests for that user must provide the User MFA.
Logging In
Once the user is enrolled in MFA, they must provide a current code from their authenticator app every time they log in. There is a little subtlety to this implementation for you, since your application may not know if users need to enter an MFA code or not. You have a couple of options. You can always show the MFA field, and let users enter it if they want. This option feels pretty lame from a UX standpoint, so we don’t recommend it. Instead, you can attempt to login without MFA and look for a special error code from TrueVault indicating that MFA is required for this user. Then you can present the MFA field and complete the login. Our sample app follows this flow.
Try to login without MFA
The login handler tries to log in without MFA the first time. Then if it sees USER.MFA_CODE_REQUIRED
in the response, it knows this user needs to provide MFA to log in. It shows the MFA field and waits for a resubmit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
authFormEl.addEventListener("submit", e => { e.preventDefault(); authValidationErrorEl.style.display = 'none'; let authPromise; if (location.hash === '#sign_up') { authPromise = registerUser(this.username.value, this.password.value); } else { authPromise = loginUser(this.username.value, this.password.value, this.mfa_code.value); } authPromise .then(response => { tvUser = response.user; location.hash = '#tasks'; }) .catch(error => { // This error could be a bad PW, or maybe the user needs to provide MFA authValidationErrorEl.style.display = ''; authValidationErrorEl.textContent = error.message; // This means the user tried to login without an MFA Code, but they are // enrolled in MFA Easy enough to solve: just give them a field to enter the MFA if (error.response.error.type === 'USER.MFA_CODE_REQUIRED') { // Show the MFA form field, it was previously hidden by default mfaCodeRowEl.style.display = ''; } }); }); |
Complete log in with MFA
The user will see a screen like this:
When they enter the code and hit the login endpoint again, they’ll be successfully logged in.
Note that the application hits the same login endpoint each time. That endpoint optionally takes the MFA Code, and if you omit it when it should be given, it responds with the USER.MFA_CODE_REQUIRED
error type.
Unenrolling
It’s important to support unrenrolling in your application. Even if all of your users are bought into the value of MFA and enroll religiously, you’ll still need to support un-enrolling when they get a new phone.
Unenrolling can be a juicy attack, so we want to make sure a simple session-hijack style attack wouldn’t be able to complete unenrollment. To do this, we require both the user’s password, and a current MFA token. (Note: if the user has lost their phone, you’ll need to contact support@truevault.com
to forcibly unenroll their device). The unenroll form looks like this:
And the handler is pretty straight forward:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
unenrollMFAFormEl.addEventListener("submit", e => { e.preventDefault(); unenrollMFA(tvUser.access_token, tvUser.id, this.unenroll_mfa_code.value, this.unenroll_mfa_password.value) .then(() => { tvUser.mfa_enrolled = false; refreshState(); }) .catch(error => { alert(error.message); }); }); |
The unenroll request makes the request to TrueVault:
1 2 3 4 5 6 |
function unenrollMFA(accessToken, userId, mfaCode, password) { return tvRequest(accessToken, "POST", `users/${userId}/mfa/unenroll`, { mfa_code: mfaCode, password: password }); } |
Then the user can start from the beginning and enroll their application again.
That’s it!
We hope this sample application makes it crystal clear how to add MFA to your application. If you have any lingering questions, check out our User MFA Tutorial or ask on Stack Overflow.