-
Authentication flow
and how we can improve it
Let's talk about authentication flow and how we can improve it -
Problem
While researching our main customer flow, I find out that a quarter of customers drop on the authentication step. I see there great opportunity to improve conversion. -
This particular chart was build based on feedback collected from users that decide to leave this flow. -
Why not?
- Not enough trust
- Not enough information
- Too complex
But why not? Why do they not want to share their private data? There could be multiple reasons: not enough trust, not enough information, or it's just plain too complex. -
Trust
User should think, that it's safe to use your service
Trust: user should think, that it's safe to use your service. -
In 2021 report, LastPass shows that each password reused approximately thirteen times. When a user enters his password in your service, he gives you a lot of trusts. -
Year later it appears that 65% of people still reuse their passwords -
And only one third stop doing this after getting correspondent education. -
How to build trust?
Show rating and review, make sure that you are responsive and available in used media-channel, add microformats
Trust: show rating and review, make sure that you are responsive and available in used media-channel, add microformats -
I was amazed to see it works. After we have used schema to add this kind of widgets, we gain more trust, and this cut share of customers leave this flow. -
Transparency
Explain why do you need information and how you are planing to use it. And how you wouldn't use it.
Transparency: explain why do you need information and how you are planing to use it. And how you wouldn't use it. -
We add copy that explains: we wouldn't share customer contact data with anyone until the customer explicitly willing to share them with service professionals -
Simplicity
Do not request data until the moment, when you need it
For example, in one of the experiments, we decide to postpone the user phone number request until we should pass it to the service professional. -
HTML
I believe that the web based on HTML, CSS and JavaScript. Also, I believe in progressive enhancement. So let's start from HTML and then see how we can iterate from there. -
Forms
Let's talk about forms. -
What forms?
- Registration (SR Flow)
- Login (SR Flow and dedicated)
- Password recovery
What forms? When we talk about authentication, it would be registration, login and password recovery forms. -
Semantics!
BIP!!!What could help us improve forms? Semantics! -
Attributes
Attributes! -
autocorrect="off" spellcheck="false" autocapitalize="none"This attributes would allow us to prevent autocorrection and autocapitalize. You wouldn't want email to autocapitalized or spellchecked, right? -
Which attributes can be useful?
- inputmode
- autocomplete
Aside from that, remember about inputmode and autocomplete attributes. They would allow you to set how to work input and autocompletion. -
inputmode
- none
- text
- tel
- url
- numeric
- decimal
- search
inputmode define how virtual keyboard on mobile phone looks like -
autocomplete
- username
- new-password
- current-password
- one-time-code
- …
autocomplete hint the brawser which autocomplete options to show -
Email
inputmode="email" autocomplete="email"For example, for email both attributes should be email -
On the iPhone you will get options for autocomplete and keyboard will be optimized to enter email. Android at first propose to fill all the form at once, and then — choose specific email. -
I would take a bet that all and every single one of you has seen how it's working on desktop. -
In the registration
or recovery form
autocomplete="new-password"Another useful for registration form attribute is autocomplete with value new-password. It make sense to use it in the registration or password recovery form, when you enter it first time. -
At iOS Safari for new-password you will see option Suggest New Password, which generate strong password, ask if you want to use it and then propose save. -
Android do the same, except if you agree to use password it will just save the strong password, without any further questions. -
And on desktop: the same story old. -
Passwords
in the authentication
process
autocomplete="current-password"current-password is the autocomplete type, which you should use in the login form when we should enter the existing password. -
iOS propose to fill password from login-passwords pairs, that was used on this site. Android will propose to will all the form at once, then just a password. Both will show login with which password was used. -
Desktop is well known and standard -
One time password
inputmode="numeric" autocomplete="one-time-code"one-time-code is … well … one time code, which usually being send as an SMS, email or generated by application. -
`${code} — your code for authentication. @${domain} #${code}`Let's look at this SMS, because it's exectly the format that would allow us to use autocomplete. -
When you will get this SMS, you will be able just click the code -
And it even more usefule, because iOS can share SMS with MacOS. -
So on the descktop you will have it as well! -
I did give you a specific SMS format, bit all this options will work as well. -
iOS simply looks for numbers next to words:
- code
- passcode
Before we actually got SMS standardised, iOS searched for numbers next to code and passcode words. -
Chrome and Android don't support a declarative way of OTP autocompletion.
Chrome and Android don't support a declarative way of OTP autocompletion. -
Origin-bound one-time codes delivered via SMS
Raad SMS standard for OTP, I mentioned before. This is a draft, but it with nuances supported by both: iOS and Android. -
Automatic Strong Passwords and Security Code AutoFill
Apple WWDC 2018 presentation, where OTP-related functionality was introdused. As well as new-password value. -
HTML Living Standard
Read about autocomplete in the HTML standard -
Also take a look at this book: «Form design patterns». If you will read it — you will make your forms even better than this. -
Where all this passwords come?!
Where did the browser get all the passwords for auto-completion? -
All your passwords
belong to us! BOP!You give it to him. -
Browser
All modern browsers allow you to save and store the credentials you enter.
All modern browsers allow you to save and store the credentials you enter. -
I think every one of you did see this kind of window in chrome or ff. You can see it by clicking on the key icon in the address bar. -
And, if it's a registration form, in the dropdown, you see different options for fields that can be considered login. -
Se-e-e-e-e-e-e-et the na-a-a-a-a-a-ame!
Не забывайте задавать имена полям. Это критически важно. -
<StyledInput ref={emailInputRef} type="email" onChange={setEmail} value={email} autoComplete="email" />Yes, we are developing react applications and rarely properly send forms nowadays. We even less often have time for graceful degradation, and our app would only work with JavaScript anyway. There is a temptation not to set a name on the field. -
But if we wouldn't set the name, there is a chance to see this: phone number as a login. How come? -
Chrome and Firefox
- username
- field, that located in the DOM before password field
Chrome and FF search for a field with the name value username or autocomplete username. If they didn't find any — they will use a field that would be found right before password field. -
Safari
- username
- field, that located in the DOM before password field
Safari is a bit more cautious, after username it search for email, only then take the field before the password input. -
Help the browser
understand
where is the username
autocomplete="username email"Keeping this in mind, I recommend you use double value for the autocomplete field. And set the name attribute. -
You can find all passwords, payment credentials and addresses, which the browser did save, in the settings. -
You can control addresses saved by the browser, delete and edit. -
And, even more, you can add a new one. And this does make perfect sense, keeping in mind that you will be able to save a lot of time by browser autocomplete. -
Sync
- Google Password Manager
- Firefox
- iCloud Keychain
Aspecially keeping in mind that this data will be synced across devides. -
Google Password Manager -
To sync password it's enough to log in into google account. -
Google will, without hustle, try to turn on sync as well. In any menu, you can click «Manage your Google Account». -
And then — «Password Manager» -
You will get to the page with your saved credentials -
In FF click the «Passwords» in the menu -
You will get to the page with your saved credentials -
iCloud Keychain -
Open Safari settings, Passwords tab. -
iOS. Ok, how phone will sync with the desktop? -
In iOS you can set the source for autocompletion. By default it's iCloud Keychain. But you can select one more. It can be Google Password Manager Or FireFox. -
Changing the password
-
Aside from autofill browser can detect compromised passwords. And, even more, you can change them directly from browser! Well … almost. -
server.use( "/.well-known/change-password", (req, res) => { res.redirect("/reset-password"); });This button by default will just open domain, related to the password. But we can use this URL, to make redirect to the page were customer can actually change it. It can be password recovery page, or specific one. This code will work for any express-like server. -
server { rewrite ^/.well-known/change-password$ http://domain.com/reset-password redirect; }And this — in the NGINX configuration. -
Expected responces
- 302 Found
- 303 See Other
- 307 Temporary Redirect
URL should return one of this statuses. -
In the Safari it work from 2019 -
But FireFox just only planning to introduce that. -
A Well-Known URL for Changing Passwords
This is a standard, that describe, how it works. -
JavaScript
I think you are pretty-much tired from HTML, so let's look what we can do with JavaScript. -
Credential Management
Almost everything we will talk about is described in the Credential Management specification. It's a very unusual one: it's extendable. -
navigator.credentialsnavigator.credentials is the base for all things. This object gives us four primary methods to work with credentials. -
.create(CredentialCreationOptions) .store(Credential) .get(CredentialRequestOptions) .preventSilentAccess().create allow us to create credentials object, .store — save it into the browser, .get let as get it back. .preventSilentAccess turn off silent login, wich we will discuss later in details. -
Credential types:
- PasswordCredential
- FederatedCredential
- OTPCredential (WebOTP)
- PublicKeyCredential (WebAuthn)
- PaymentCredential
Today we have five types of credential, and we wouldn't touch PaymentCredential, because it would be too much for this talk. -
PasswordCredential
Authentication by login and password.
The basic case: login and password. -
-
CredentialData:
- id
When we create credential objects, CredentialData type will be the base for all of them. And it have only one single attribute — "id". -
PasswordCredential:
- name
- iconURL
- password
- type: "password"
PasswordCredential extend CredentialData and add there fields name, iconURL, password and type. Which always will have value «password». -
We have registration form. When customer has clicked button "Register"… -
const signUpHandler = async (event) => { // Registration const form = event.currentTarget; if (window?.PasswordCredential) { const credential = await navigator. credentials.create({ password: form }); navigator.credentials.store(credential); } }We can create credential object right from the form. And save it into the browser. -
const signUpHandler = (event) => { // Registration const form = event.currentTarget; if (window?.PasswordCredential) { const credential = new PasswordCredential(form); navigator.credentials.store(credential); } }We can make the code bit more readable by using PasswordCredential constructor. -
Curious fact: if customer using strong password generated y browser, then browser will just save credentials, without need in any additional permission from the customer. -
Otherwise customer will see this popup: -
function signUpHandler (event) { // Registration const form = event.currentTarget; const data = new FormData(form); if (window?.PasswordCredential) { // … } }But what if we need more control over credentials? -
const credential = new PasswordCredential({ id: data.get('email'), name: data.get('firstName'), password: data.get('password'), iconURL: data.get('avatar'), }); navigator.credentials.store(credential);We can create object manually. This will allow us to add avatar image, or set which field will be used as a username. -
UI, as a result, can look much more appealing. -
navigator.credentials.get()Ok, now, when we did save the credentials, how we can use them? We can get them from the browser with get method. -
CredentialRequestOptions:
- mediation
- silent
- optional
- required
- signal
You should pass there CredentialRequestOptions object as an argument. And now, it is important to know what mediation is. - mediation
-
CredentialRequestOptions:
- mediation
- silent
- optional
- required
- signal
silent means that browser should return credentials without any interaction from the customer side, if it's possible and null otherwise. Browser can do that if:
1. Browser do have credentials for this origin.
2. Browser have only one set of credentials for this origin.
3. Prevent Silent Access method was not called. - mediation
-
CredentialRequestOptions:
- mediation
- silent
- optional
- required
- signal
optional meand that if browser can return credentials without need of customer interaction — it will do that. Otherwise it will show customer a popup to select which credential he want to use, if there more then one. And null if no credentials for this origin or customer closed the popup. - mediation
-
CredentialRequestOptions:
- mediation
- silent
- optional
- required
- signal
required means that browser always will see the popup, that request his permission. - mediation
-
CredentialRequestOptions:
- mediation
- silent
- optional
- required
- signal
signal is just an instance of AbortSignal. Which allow us to close popup programmatically. - mediation
-
PasswordCredential:
- password: true
PasswordCredential extend CredentialRequestOptions and add just one field: password, which have value "true". This means that we only request from browser credentials of password type. -
(async () => { // Only run code if customer not logged in if (window?.PasswordCredential) { const credential = await navigator.credentials.get({ password: true, mediation: 'silent', }); if (credential === null) return; // Authentication // Error handling }})();To make silent login, like in Twitter, we need at every page, for Anonymous customer add this widget. It request credential with mediation silent. If we will get it — great, let's auth customer. Otherwise — just do nothing. It's ok. -
PasswordCredentialData:
- id
- name
- iconURL
- password
- type: "password"
Inside credential objects is what we put there. -
In case of sucessfull authentication we will see popup -
if (window?.PasswordCredential) { const credential = await navigator.credentials.get({ password: true, mediation: 'optional', }); if (credential === null) return; // Authentication // Error handling }When customer clicked login button we can request credentials with mediation: 'optional'. Customer express wish to authenticate and popup wouldn't fill unexpected or out of place. -
If he will select account or give permission to authenticate — perfect. Otherwise we can show him normal login page. -
if (navigator?.credentials .preventSilentAccess) { await navigator?.credentials .preventSilentAccess(id); }After customer click logout, we need to call preventSilentAccess. Otherwise after refreshing the page customer will be logged back in. Which hardly wouldn't be an expected behaviour. -
But most important here is cross-platform login. With it's help we can login on a descktop and then automatically login on mobile, where it's harder to do. -
Let's see how it works! I don't have Android phone, so I will use emulator. I started it beforehand and login into my google account. -
-
FederatedCredentialData:
- id
- name
- iconURL
- provider
- protocol
- type: "federated"
Second credential type is FederatedCredentialData. Same as PasswordCredentialData, but for social login. I will not stop here because of time constrains. -
It work in Blink. So most% of customers having it. There are also marked iOS support, but it's not fully true. -
Apple saying that there are support, but in fact it's only for PublicKeyCredential. Which we will discuss later. -
It looks like Password and FederatedCredential not in the priority list. -
Mozilla don't see it's a priority as well. -
The Credential Management API (Web Fundamentals)
Read great article about Credential Management from Google. -
Credential Management
And specification as well! -
Web OTP API
Remember we discussed OTP declarative autofill and how it's not supported by Android? -
OTPCredential
It does support OTPCredential instead. Which do the same but better! -
if ('OTPCredential' in window) { const { code, type } = await navigator.credentials.get({ otp: { transport: ['sms'] } }); if (!code) return; // Authentication }Request credentials with otp type and sms transport. -
Brower will ask permission to read SMS, then return the code. -
SMS requirements:
- Message should contain 4-10 numerals. Minimum one as a number.
Message should contain 4-10 numerals. Minimum one as a number. -
SMS requirements:
- Message should be sent from the number that are not in the customer address book.
This way specification ensure that we wouldn't read customer private messaging. -
Verify phone numbers on the web with the Web OTP API (Web Fundamentals)
Great article at Web Fundamentals -
Origin-bound one-time codes delivered via SMS
Specification on SMS format -
Web OTP API
OTP related specification -
WebAuthn
-
PublicKeyCredential
-
Cross-platform
-
-
-
Platform-dependent
-
-
-
Face unlock -
-
Platform key:
- Customer register
- Request permission to use the key
- If customer confirm: register the key and save id in the browser
- When we want to authenticate customer — using the key
If we are talking about simplifying authentication we are interested in the platform keys specifically. -
Key registration:
- Send data about platform and customer to the server, getting back validation object
- Pass it to platform and get PublicKeyCredential
- Send PublicKeyCredential to server to validate
- Getting back Сredential Id, which we can store on a client
-
if ('PublicKeyCredential' in window) { /* … */ } -
const hasPlatformAuthenticator = await PublicKeyCredential ?.isUserVerifyingPlatformAuthenticatorAvailable(); -
PublicKeyCredential .is User Verifying Platform Authenticator Available(); -
const data = new FormData(form); const response = await fetch( '/challenge/register', { method: 'POST', body: JSON.stringify({ name: data.get('name'), email: data.get('email'), 'new-password': data.get('new-password'), }), }); const options = await response.json(); -
const credential = await navigator.credentials .create({ publicKey: options }); -
-
{ id: "nZOlyXf57erlQbudIIuOrQEdRUs", rawId: [ArrayBuffer], response: { attestationObject: [ArrayBuffer], clientDataJSON: [ArrayBuffer], }, type: "public-key", } -
const body = JSON.stringify(attestation); const response = await fetch(`/register`, { method: "POST", body, }); -
const { credentialId } = await response.json(); localStorage.setItem('credentialId', credentialId); -
How to login with platform key:
- Checking if we have saved Сredential Id
- Request challenge from the server
-
How to login with platform key:
- Pass challenge to the platform and getting PublicKeyCredential back
- Send PublicKeyCredential to the server, so we can check the challenge
-
const credentialId = localStorage.getItem('credentialId'); if (credentialId === null) return; -
const response = await fetch( '/challenge/login', { method: 'POST' } ); const options = await response.json(); -
const { challenge } = options; const credentialId = localStorage.getItem('credentialId'); options.allowCredentials = [{ type: "public-key", id: toBuffer(credentialId), }]; -
const credential = await navigator.credentials.get({ publicKey: options }); -
-
{ id: "FtjWT4fIT9…zs2LRxuG5fBRj9bA", rawId: [ArrayBuffer], response: { authenticatorData: [ArrayBuffer], clientDataJSON: [ArrayBuffer], signature: [ArrayBuffer], userHandle: null, }, type: "public-key", } -
const body = JSON.stringify(authenticator); await fetch('/login', { method: "POST", body }); -
-
WebAuthn demo application
-
Emulate Authenticators and Debug WebAuthn in Chrome DevTools
-
A curated list of awesome WebAuthn/FIDO2 resources
-
WebAuthn guide
-
Introduction to WebAuthn API (Yuriy Ackermann)
-
Meet Face ID and Touch ID for the web (WWDC 2020 video)
-
Meet Face ID and Touch ID for the web (Webkit blog post)
-
Web Authentication and Windows Hello
-
Enabling Strong Authentication with WebAuthn
-
Fido Alliance
-
Verifiable Credentials Data Model
-
Yubico (YubiKey)
-
Web Authentication: An API for accessing Public Key Credentials
-
-
Made by Ukraine.