Two factor authentication (2FA) is a great way to improve the security of user accounts in an application. It helps protect against common issues with passwords, like users picking easily guessable passwords or reusing the same password across multiple sites. There are different ways to implement two factor authentication, including SMS, using an authenticator application and WebAuthn.
SMS is the most widely used and won’t be going away, so it falls on us as developers to do our best to build the best SMS 2FA experience for our users. The WebOTP API is one way we can help reduce friction in the login experience and even provide some protection against phishing.
What is the WebOTP API?
The WebOTP API is an extension to the Credential Management API. The Credential Management API started by giving us the ability to store and access credentials in a browser’s password manager, but now encompasses WebAuthn and two factor authentication. The WebOTP API allows us to request permission from the user to read a 2FA code out of an incoming SMS message.
When you implement the WebOTP API the second step of a login process can go from an awkward process of reading and copying a number of digits from an SMS, to a single button press. A great improvement, I think you’ll agree.
How does it work?
To implement WebOTP you will need to do two things:
- Update the message you send with the WebOTP format
- Add some JavaScript to the login page to request permission to read the message
The SMS message
To have the WebOTP API recognise a message as an incoming 2FA code you need to add a line to the end of the message that you send. That line must include an @
symbol followed by the domain for the site that your user will be logging in to, then a space, the #
symbol and then the code itself. If your user is logging in on example.com
and the code you are sending them is 123456
then the message needs to look like this:
Your code to log in to the application is 123456
@example.com #123456
The domain ties the message to the website the user should be logging onto. This helps protect against phishing, WebOTP can’t be used to request the code from an SMS if the domain the user is logging in to doesn’t match the domain in the message. Obviously it can’t stop a user copying a code across from a message, but it might give them pause if they come to expect this behaviour.
The JavaScript
Once you have your messages set up in the right format you need some JavaScript on your 2nd factor page that will trigger the WebOTP API, ask the user permission for access to the message and collect the code.
The most minimal version of this code looks like this:
if ('OTPCredential' in window) {
navigator.credentials.get({
otp: {
transport: ['sms']
}
}).then((otp) => {
submitOTP(otp.code);
});
}
We ask the navigator.credentials
object to get a one time password (OTP) from the SMS transport. If the browser detects an incoming message with the right domain and a code in it, the user will be prompted, asking for access. If the user approves the promise resolves with an otp
object which has a code
property. You can then submit that code to the form and complete the user’s login process.
A more complete version of the code, that handles things like finding an input and form, cancelling the request if the form is submitted, and submitting the form if the request is successful, looks like this:
if ('OTPCredential' in window) {
window.addEventListener('DOMContentLoaded', e => {
const input = document.querySelector('input[autocomplete="one-time-code"]');
if (!input) return;
const ac = new AbortController();
const form = input.closest('form');
if (form) {
form.addEventListener('submit', e => ac.abort());
}
navigator.credentials.get({
otp: { transport:['sms'] },
signal: ac.signal
}).then(otp => {
input.value = otp.code;
if (form) {
form.submit();
}
}).catch(err => {
console.error(err);
});
});
}
This will work for many sites, but copying and pasting code isn’t the best way to share code, so I came up with something a bit easier.
Declarative WebOTP with web components
On Safari, you can get similar behaviour to the WebOTP API by adding one attribute to the <input>
element for the OTP code. Setting autocomplete="one-time-code"
will trigger Safari to offer the code from the SMS via autocomplete.
Inspired by this, I wanted to make WebOTP just as easy. So, I published a web component, the <web-otp-input>
component, that handles the entire process. You can see all the code and how to use it on GitHub. For a quick example, you can add the component to your page as an ES module:
<script type="module" src="https://unpkg.com/@philnash/web-otp-input"></script>
Or install it to your project from npm:
npm install @philnash/web-otp-input
and import it to your application:
import { WebOTPInput } from "@philnash/web-otp-input";
You can then wrap the <web-otp-input>
around your existing <input>
within a <form>
, like this:
<form action="/verification" method="POST">
<div>
<label for="otp">Enter your code:</label>
<web-otp-input>
<input type="text" autocomplete="one-time-code" inputmode="numeric" id="otp" name="otp" />
</web-otp-input>
</div>
<button type="submit">Submit</button>
</form>
Then the WebOTP experience will happen automatically for anyone on a browser that supports it, without writing any additional JavaScript.
WebOTP: a better experience
The WebOTP API makes two factor authentication with SMS a better experience. For browsers that support it, entering the code that is sent as a second factor becomes a breeze for users.
There are even circumstances where it works for desktop browsers too. For a user with Chrome on the desktop and Chrome on Android and signed into their Google account on both, signing in on the desktop will cause a notification on the mobile device asking to approve sending the code to the desktop. Approving that on the mobile devices transfers the code to the desktop browser. You don’t even have to write more code to handle this, all you need is the JavaScript in this article.
For more on WebOTP, check out these articles:
- Verify phone numbers on the web with the WebOTP API
- Verify a phone number on desktop using WebOTP API
If you are building two factor authentication or phone verification, consider implementing the WebOTP API as well to make that process easier for your users.
Top comments (14)
This is nice!
Yeah it is! And I hope to see it implemented more!
Hey, it was a nice read, you got my follow, keep writing!
Thanks very much!
Hi Phil. Does it still work normally for you?
I can't get it to work at all. With the JS code, with your component, with the sender being out of my contacts. The message arrives, but the auth dialog does not appear and, consequently, does not read the message. Weird...
Do you have the domain set up correctly in the SMS that you are sending?
Yes.
@domain.com #123
I`ve tried with your compenent also. Here is my test, if u dont mind to take a look whenever u want: terra.com.br/especial/dsv/form/otp...
Do you get errors in the console at all?
nothing at all....
Your code looks like it should work fine. I don't currently have an Android device to test it out, though I'm also not sure how you trigger the SMS for the form at that link.
What device/browser are you trying this on.
Im testing on android/chrome. Im also testing your component here: terra.com.br/especial/dsv/form/otp...
Sometimes I receive the permission dialog, sometimes not. Really weird.
Thanks good information (I just wish we won’t need it anymore). :)
I remember when I was implementing for the first time U2F and OTP using browser capabilities. It was so messy. I see in your article OTP is easier now, but what about U2F? Is it any better right now?
While implementing U2F uses the same
navigator.credentials.get
andnavigator.credentials.create
methods, I do find that getting the right options to pass is a bit of a mess. There are just the base the methods though, so hopefully someone can build something that makes it easier while retaining the power of it. Especially as the public key credential is also the basis of passkeys, which have the potential to replace passwords.