DEV Community

Cover image for Using a Single Input for one-time-code
Mads Stoumann
Mads Stoumann

Posted on

Using a Single Input for one-time-code

The other night I had a bit of fun trying to create a single-input one-time-code:

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.

From Wikipedia:

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}"
>
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, no browsers support caret-shape yet.


Another way, would be to add another background-gradient, but without repeating the pattern:

Image description

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

Could this be done in an even simpler way? Other sugestions? Please use the comments!


Here's a Codepen-demo:

Top comments (24)

Collapse
 
mindplay profile image
Rasmus Schultz

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?

Collapse
 
mellen profile image
Matt Ellen • Edited

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.


  1. en.wikipedia.org/wiki/Multi-factor...
Collapse
 
mindplay profile image
Rasmus Schultz

I'm not sure if this really answers my question. 😅

According to the wikipedia entry you quoted in your article:

An email is one of the common ways of using OTACs. There are two main methods used. With the first method, a service provider sends a personalised one time URL to an authenticated email address e.g. @ucl.ac.uk; when the user clicks the URL, the server authenticates the user.[25] With the second method, a service provider sends a personalised OTAC (e.g. an enciphered token) to an authenticated email address; when the user types the OTAC into the website, the server authenticates the user.

More often than not, I'll get these codes via email.

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 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. 🤔

Thread Thread
 
mellen profile image
Matt Ellen • Edited

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:

a number of implementations [of OTPs] also incorporate two-factor authentication by ensuring that the one-time password requires access to something a person has (such as a small keyring fob device with the OTP calculator built into it, or a smartcard or specific cellphone)

An email account is not something physical a person has, as detailed in the MFA article I linked to:

Something the user has: Any physical object in the possession of the user, such as a security token (USB stick), a bank card, a key, etc.

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.

Thread Thread
 
mellen profile image
Matt Ellen

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.

Thread Thread
 
mindplay profile image
Rasmus Schultz

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...

The security measures should be compatible with the level of risk involved in the payment service. In order to allow the development of user-friendly and accessible means of payment for low-risk payments, such as low value contactless payments at the point of sale, whether or not they are based on mobile phone, the exemptions to the application of security requirements should be specified in regulatory technical standards. Safe use of personalised security credentials is needed to limit the risks relating to phishing and other fraudulent activities. In that respect, the user should be able to rely on the adoption of measures that protect the confidentiality and integrity of personalised security credentials. Those measures typically include encryption systems based on personal devices of the payer, including card readers or mobile phones, or provided to the payer by its account servicing payment service provider via a different channel, such as by SMS or email. The measures, typically including encryption systems, which may result in authentication codes such as one-time passwords, are able to enhance the security of payment transactions. The use of such authentication codes by payment service users should be considered to be compatible with their obligations in relation to payment instruments and personalised security credentials also when payment initiation service providers or account information service providers are involved.

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?

Thread Thread
 
madsstoumann profile image
Mads Stoumann • Edited

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" ;-)

  1. When you log into any government webpage, you need to fill out your MyId-username, and click on sign-in.
  2. Then you open your MyID-app, and log in with either a biometric method (face or finger) or passcode.
  3. Next, you need to scan a QR-code on the webpage you're visiting
  4. Finally, you can swipe the "Sign in"-button in the app, and ...
  5. The webpage redirects you to the signed-in area
Thread Thread
 
mindplay profile image
Rasmus Schultz

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. 🙂

Thread Thread
 
madsstoumann profile image
Mads Stoumann

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!

Thread Thread
 
madsstoumann profile image
Mads Stoumann • Edited

This is what I’m talking about: mitid.dk/hjaelp/hjaelpeunivers/mit...

Collapse
 
madsstoumann profile image
Mads Stoumann

Exactly! Thanks for clarifying 2FA!

Collapse
 
jamiewarb profile image
Jamie Warburton • Edited

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.

Collapse
 
dbolser profile image
Dan Bolser

1) lazy devs copy patterns without thinking.
2) the code let's you easily cross platform, e.g. laptop/mobile

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you! Good idea for the credit cards-entry. I'd look into that + Temanis box-gradient-example ;-)

Collapse
 
afif profile image
Temani Afif

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)

Collapse
 
madsstoumann profile image
Mads Stoumann

Nice! Will try the "box" for the "current position"

Collapse
 
hayley_grace profile image
Hayley Grace

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!

Collapse
 
madsstoumann profile image
Mads Stoumann

Hi,
Try fiddling with padding-inline-start. Change the 2 in the division to 1.5 or smaller, to move the text right.
/Mads

Collapse
 
koas profile image
Koas

Amazing article! And the final result works great, congratulations!
Also, kudos for using logical properties! 👌🏻

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
yjdoc2 profile image
YJDoc2

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 😄

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Thanks

Collapse
 
ritikbanger profile image
Ritik Banger

I have created a npm package- react18-input-otp, you can also use that.