All notes
ArchitectureOctober 24, 20267 min read

Proving it's yours: the second half of one-tap sign-in

Subhankar Denria

CTO & product engineer

Proving it's yours: the second half of one-tap sign-in
~7 min

What this part does

Make "Continue with Google" safe to join an existing account, give people a way back when they forget a password, and send the email that needs, from a server with no IPv4 address.

Built
In two days
Closes
A quiet gap in how accounts were joined
Costs
Still $0 a month

The first half of this story ended on a rule: when someone taps Continue with Google or Apple, and an account already has that email, join it instead of making a second one. People who signed up with a password find everything still there.

It's the right rule. It also leaned on something nobody had checked: that the email typed at sign-up belonged to whoever typed it. Anyone could sign up with any address. If someone ever made an account in another person's name, the real owner, tapping Continue with Google later, would be joined into an account whose password somebody else had chosen.

Unlikely, but real. This is how it closed, and what came with it.

When one tap needs one more step

Most taps still need nothing extra. The new step appears in exactly one case, once:

Someone taps Continue with Google

No account has this email yet

A new account, in one tap

The account was made with this Google account

Straight in, in one tap

The account was made with a password, and its email never proven

Prove it once: that password, or a code to the inbox

The account is already joined to a different Google account

Refused, never taken over

Four ways to close it were on the table:

Four ways to close it

Ask for that account’s password once

What a person sees: One sheet, once

Chosen

Prove the inbox with an emailed code

What a person sees: A six-digit code

Added a day later

Keep joining as before

What a person sees: Nothing

The gap stays open

Replace the password, sign everyone out

What a person sees: Nothing, until it matters

Never

Asking for the account's password once is the standard answer for an email that was never proven: Google, Firebase and Auth0 all advise it. The emailed code came a day later, as soon as the server could send email. It also covers the case that matters most: if someone else chose the password, the real owner can still prove the inbox is theirs.

The one option that was never on the table: replace the password and sign everyone else out. In this app, a phone that's quietly signed out stops doing its job, and nobody would notice until it mattered.

What a person sees

A sheet slides up, once. Two marks join at the top: Google's (or Apple's) and their own account, with a line of gold dots between them. Try both ways through it:

Try it: the sheet, once

This one’s already yours

Your password, just once. Then Google opens it.

In this demo the password is open sesame, and any six digits work.
  • The right password or code: in, and from then on it's one tap.
  • The wrong one: it says so, and stays.
  • Not now: nothing changes.
  • Nobody is signed out anywhere. The password keeps working on their other phones.
  • An account already joined to a different Google account is refused, never taken over.

Once joined, the email counts as proven, so a later Continue with Apple with the same address goes straight in.

Two smaller fixes came along. Signing up again with an email in use used to show the framework's own "The email has already been taken." It now says what to do: "This email already has an account. Tap 'I already have an account' to sign in." And under Continue with Apple there is a hint: "On Apple's screen, choose 'Share My Email'." Pick "Hide My Email" instead, and the app receives a relay address it can't match to the account you already have.

A server with no IPv4 needs to send email

Codes need email, and the server couldn't send any. It had never needed to.

The obvious choice was the provider that already runs the domain's inbox. It answers only on IPv4, as do most email services, and this server has only IPv6. The same lesson as Apple, a week later. So each service was checked, the same way:

dig AAAA, for every candidate

The inbox provider the domain already uses

…

Most email sending services

…

One sending service’s API

…

The first test email, as Gmail saw it

SPF DKIM DMARC

One sending service's API does answer on IPv6. It's free for 300 emails a day with no card, and signs mail as the app's own domain. The domain got two DKIM records, and its single SPF record now lists both the inbox provider and the sending service. A daily check keeps the API key from lapsing unnoticed. The first test emails landed in Gmail with SPF, DKIM and DMARC all passing.

"Forgot password?", with a six-digit code

Three steps: Forgot password? under the password box, then Email me a code with the address already filled in, then the code and a new password. Signed in, and nobody signed out elsewhere.

The page answers exactly the same whether or not the email has an account, so it can't be used to find out who uses the app.

The code box got some care. Six places on a line, split 3 + 3. The next place glows gold, and when all six are in, the light runs along the line. Pasting fills it at once, and an iPhone offers the code from the email above the keyboard. Type one:

Try it: type six digits, or paste them

6 to go

  • Six digits
  • Works for 15 minutes
  • Five tries, then it’s dead
  • A new code ends the old one
  • One a minute, five an hour, per address
  • Stored only as a keyed hash

The email itself is deliberately plain:

The email

From the app · just now

Your code: 482913

Your code is

482 913

It works for 15 minutes.

If you didn’t ask for it, you can ignore this email. Nothing changes.

No linksNo buttonsNo trackingNo images

The code is in the subject, whole, because that's what phones read when they offer it. And with no links at all, there's nothing in it for a fake to imitate.

Signing out, properly

Two things I noticed myself after signing out, both fixed in the next release.

Stale screens. After signing out, one screen still showed the old account's people, and saving said the session had ended. Signing out deleted the copy kept in storage, but not the copy the screens held in memory. Nothing leaked and nothing changed on the server, but it was wrong.

A slow door. Signing out waited for network housekeeping, with nothing on screen. On a phone with no signal, that wait had no end.

Tap Sign out

Before

Wait: forget the phone’s push address

A network trip. With no signal, no end

Wait: wipe the phone’s local data

Nothing on screen meanwhile

The front door, at last

Screens still remember the old account

Storage was cleared; memory was not

Next release

“Signing out…”: tell the server first

The one wait that matters, so nothing is sent by mistake

The front door

Every screen told the account is gone

In the background: push address, local wipe

They finish on their own time

Quick, and clean

What I'd tell anyone joining accounts

  1. 1An email typed into a form proves nothing. Before you join anything to it, ask for the account's password once, or prove the inbox with a code.
  2. 2Never fix a security gap by signing everyone out if signing out has side effects your users can't see.
  3. 3Check every outside service for IPv6 before you depend on it, email included: dig AAAA <host>.
  4. 4Make "forgot password" say the same thing for every address, so it can't be used to look people up.
  5. 5Signing out must clear memory, not just storage, and should never make anyone wait for housekeeping.

1,056 tests guard the app and the server, all passing.