The other night I had a bit of fun trying to create a single-input one-time-code:
one-time-code is a valid autocomplete
-value, and with a few lines of JS, it let's you fill out the field from a text-message (sms).
More often, it's called OTP, though.
A one-time password (OTP), also known as a one-time PIN, one-time authorization code (OTAC) or dynamic password, is a password that is valid for only one login session or transaction, on a computer system or other digital device.
Back to my example.
It's using very simple markup:
<input
type="text"
autocomplete="one-time-code"
inputmode="numeric"
maxlength="6"
pattern="\d{6}"
>
The CSS is a bit more complex:
:where([autocomplete=one-time-code]) {
--otp-digits: 6; /* length */
--otc-ls: 2ch;
--otc-gap: 1.25;
/* private consts */
--_otp-bgsz: calc(var(--otc-ls) + 1ch);
all: unset;
background: linear-gradient(90deg,
var(--otc-bg, #EEE) calc(var(--otc-gap) * var(--otc-ls)),
transparent 0
) 0 0 / var(--_otp-bgsz) 100%;
caret-color: var(--otc-cc, #333);
clip-path: inset(0% calc(var(--otc-ls) / 2) 0% 0%);
font-family: ui-monospace, monospace;
font-size: var(--otc-fz, 2.5em);
inline-size: calc(var(--otc-digits) * var(--_otp-bgsz));
letter-spacing: var(--otc-ls);
padding-block: var(--otc-pb, 1ch);
padding-inline-start: calc(((var(--otc-ls) - 1ch) / 2) * var(--otc-gap));
}
It's a bunch of stuff simulating 6 fields (from the property --otc-digits
), while — in reality — it's just a single <input>
. The spacing between the "fields" is due to letter-spacing
, and the gray "boxes" are from a linear-gradient
.
It has to use a monospace-font, so the magic value of 1ch
works — same applies to the letter-spacing
. 1ch
equals the width of a zero.
But why?
Have you ever created a OTP-component before?
I'm writing "component", because it's typically a <fieldset>
with six <input>
s and a bunch of JavaScript to detect when you enter- or leave a field etc.
When you fill out the field from the Web OTP API, you need to split the value, and fill-in six fields instead of one.
With a single input it's much simpler:
navigator.credentials.get({
otp: {transport:['sms']}
})
.then(otp => input.value = otp.code);
Highlightning the current "field"
The single-input OTP is not perfect. When you move from "field" to "field", it would be a better user-experience, if the caret was a block:
.selector {
caret-shape: block;
}
Unfortunately, no browsers support caret-shape
yet.
Another way, would be to add another background-gradient
, but without repeating the pattern:
And position it by multiplying the size-property — --_otp-bgsz
— with the digit-number, --_otp-digit
, as a custom property:
.selector {
background-position:
calc(var(--_otp-digit, 0) * var(--_otp-bgsz)) 0;
}
This is not perfect, because we'd need to put the digit in a CSS Custom Property, and then update that with JavaScript:
input.addEventListener('input', () =>
input.style.setProperty('--_otp-digit',
input.selectionStart)
)
Could this be done in an even simpler way? Other sugestions? Please use the comments!
Here's a Codepen-demo:
Top comments (24)
First, I love that you went through the trouble of doing this properly! 👍
I've seen a bunch of sites where you can't, for example, copy and paste the code, because they're individual inputs, which is just super annoying.
And you don't even mention accessibility, but I'm sure this is much better than 6 inputs for people using a screen reader as well.
You could probably adapt this approach to work for credit card number inputs as well? Those are also often some awful combination of multiple inputs, or key event handlers replacing underscores and moving the cursor around, yeesh. 😅
Now, I have to ask though - for these one time codes, why do people use inputs at all? 🤔
I mean, couldn't you just send me a link with the same code in a query string, so I could continue instantly, by just tapping the link? That would be so much simpler and more convenient.
Is that any less secure, or why do people do it this way in the first place? Do you know?
OTP is meant to be the "something you have" factor1 (as opposed to your password, which is the "something you know" factor). So that's why the generators are on your phone (and not linked to any kind of account) or sent via SMS to your phone (which is less secure due to the possibility of SIM cloning), or done with some external bit of hardware.
Your email account is not a physical thing, so it is not a "something you have" factor, it is a "something you know", as it can also be accessed via a password.
I'm not sure if this really answers my question. 😅
According to the wikipedia entry you quoted in your article:
More often than not, I'll get these codes via email.
I don't think you are correct about that.
From my understanding, "something you know" refers to information that only the user should know, such as a password, a PIN, or a security question answer.
Whereas "something you have" would involve possessing a physical or digital object that only the user should have access to, such as a smartphone, a hardware token, or an email account.
If you're a bank, you obviously shouldn't use email for this (because of possible email interception, account recovery risks, etc.) but plenty of online shops and services use them - and in a 2FA scheme, I'm fairly certain your email account is considered "something you have".
If your implementation doesn't use or allow email, you're probably not the right person to answer my original question though. The Wikipedia article only says those two forms of authentication exist - what I'd really like to know is why anyone would use a code you have to copy, rather than a link you can just click. It's much less convenient, and from my understanding seems to equally prove that you had access to that email account, so what would be the point of using anything other than a link. 🤔
Yes, a one time password can be received by email, such as a password reset request, but that's not the same as the OTP generated by e.g. Authy.
To quote the article you quote from:
An email account is not something physical a person has, as detailed in the MFA article I linked to:
It can be argued that if your email is secured by 2FA then it could proxy 2FA, but I think that would not be enough in most instances, as there are ways around having to use 2FA to log in.
An email account is "something you know" in essence, not literally, as it is possible to access it with only something you know, e.g. if it's only secured with a password, so essentially proxies that factor.
There are apps, e.g. the github app, that act as a second factor that don't require a user to input a OTP, which I think fits your request.
When I make an online payment, I log into my banking app, approve the payment there and then click the button in the website, no user input OTP required.
A link in an email does not count as a second factor.
Just to clarify, the OTP generated by e.g. Authy is (supposedly) proof that you have a physical object, i.e. your phone. That's what makes it a second factor.
A link in an email can never prove you have a particular physical object, so it cannot be a second factor.
I think this can vary - it seems to depend on what is acceptable as a "factor" in a given (legal) context.
For example, email appears to be good enough for e-commerce here in the EU:
eur-lex.europa.eu/legal-content/EN...
From my understanding, two-factor authentication (2FA) has no formal definition or standard - so what goes as a factor is going to depend on the specific context.
As said, many shops and services do use email for this - they do refer to this as "two-factor" or "multi-factor" authentication, and in that context, your email account itself (and not the code you use to log into that email account) is considered as "something you have".
I think the definitions are going to vary depending on who you're talking to, what they're authenticating access to, what rules and regulations apply, and so on?
It's a broad topic, for sure, and — as you mention — some of the 2FA-options are not needed. For instance, HumbleBundle and Steam send you an email with a passcode, you need to enter.
That could just as well be a link.
GitHub use email/password and then a 2FA-method of choice.
I'm using a YubiKey, with a text-message as fallback.
On a MacBook with touch-login, you can set that up as a 2FA-method as well (also with Github)
For me, the OTP makes sense when you're logging in from a device capable of receiving a text-message, so you can just auto-fill the code through the API. Otherwise, it can be annoying.
And speaking of annoying, let me tell you about a system we have in Denmark called "MyID" ;-)
I'm actually Danish too. 😄
I think the process you're describing is when you first connect your bank (or other service) to your ID? When I confirm a purchase or log into online banking, I just log in (username and password) and open the app on my phone, then swipe to confirm. 🙂
Hej! I think it's a new thing, when you're not logging in from the device with the app, ie. your computer. At least I had to scan the QR-code and all that for the various sites related to having a company — and it was very annoying!
This is what I’m talking about: mitid.dk/hjaelp/hjaelpeunivers/mit...
Exactly! Thanks for clarifying 2FA!
You're describing multi-factor authentication (or 2FA as it's often called). That's where your link points too. For OTP, you want this article: en.wikipedia.org/wiki/One-time_pas...
OTP is often used for multi-factor auth, but neither are exclusive to each other. For example OTP can be used in place of a password for single-factor authentication, and a physical keyfob could be used for multi-factor.
For the original question, the reason I offer OTP instead of just a link is two-part.
Firstly, it means I can get the code on one device, and enter it on another. E.G. access my emails on my mobile to get the OTP, and enter it on my laptop.
And secondly, for when you already have a window open which knows where I want to go next after login. If I click on a link, it's going to instead take me to a generic post-login page, but if I enter the code in my existing flow I can continue in that flow.
This is also useful for OAuth 2.0, so that I can continue whatever action on site A I was doing after authenticating on site B via the OTP. Otherwise I'd need to go back, refresh, and do my action again.
My preferred implementation would have both a code and a link, so the user can decide.
1) lazy devs copy patterns without thinking.
2) the code let's you easily cross platform, e.g. laptop/mobile
Thank you! Good idea for the credit cards-entry. I'd look into that + Temanis box-gradient-example ;-)
Reminds a similar idea I did to style numbers and put them inside boxes: css-tip.com/number-inside-box/ (I also made a border-only version)
Nice! Will try the "box" for the "current position"
This is really cool, and has been pretty helpful for me so far. I am having trouble though, where the cursor sits at exactly the left edge of the grey square so the numbers end up positioned to the left of the 'input' instead of in the middle. Which property is it that controls this position? Is it the padding-inline-start? I've played around with the code fiddle but while it works in there it doesn't work in my React project as nicely. Any help appreciated!
Hi,
Try fiddling with
padding-inline-start
. Change the2
in the division to1.5
or smaller, to move the text right./Mads
Amazing article! And the final result works great, congratulations!
Also, kudos for using logical properties! 👌🏻
Thank you!
Hey really nice article! The actual implementation is pretty cool, and I really liked reading the blog itself too :) Nicely short and sweet! Thanks for writing and sharing 😄
Thank you!
Thanks
I have created a npm package- react18-input-otp, you can also use that.