Add a complete login to your site

Authentication is a week of work and a decade of edge cases. Most of the edge cases are the same everywhere.

Adding "sign in with GitHub" looks like an afternoon. There is a client id, a redirect, and a token exchange, and the happy path works quickly enough to feel finished.

What takes the decade is everything the happy path does not touch. What happens when the same person signs in with a second provider. What happens when a provider returns an email it never verified. What happens when two browser tabs complete the same flow at once. What happens when someone signs in with X, then later with the password account they already had, and finds an empty workspace.

None of that is specific to your product, which is why it is worth not writing again.

The parts that are genuinely hard

The OAuth redirect is the easy half. These are the parts that generate the bug reports:

  • State and PKCE. Without an unguessable state parameter bound to the flow, an attacker starts a login of their own and feeds you the callback — and the victim ends up signed into the attacker's account, which is a subtle enough failure that it usually ships.
  • Verified versus claimed email. Matching a provider profile to an existing account by email is account takeover with extra steps unless the provider vouched for that address. An attacker registers a provider account claiming victim@company.com and inherits everything.
  • Account linking. Facebook and X do not return a verified email at all, so a user who already has a password account gets a second, empty one. The fix is not smarter matching; it is making linking an operation on a session the user has already proved they hold.
  • Races. Two tabs, a double-clicked button, a retried callback — all of them can attach one provider identity to two accounts unless the write is an atomic claim rather than a read-then-write.
  • Unlinking. Removing the last way into an account locks somebody out permanently, and for an encrypted vault no support process can undo it.

How the design answers them

Sign-in and linking are two different questions, and treating them as one is where most implementations go wrong.

Sign-in asks "who is this?" and answers only from an identity we have seen before, a provider-verified email, or by creating a new account. Never from an unverified claim.

Linking asks "attach this provider to the account I am currently signed into" — the user has already proved they hold it, so nothing is inferred and there is nothing to spoof. The user with two accounts is not stuck: they sign into the original and press Connect. One extra step, once, and it is the step that makes the operation safe.

The identity index is a claim, not a lookup

Each provider identity is written with a create that fails if the record already exists, rather than a read followed by a write. Two flows racing on the same identity — the double-clicked button, the retried callback — would otherwise attach it to two different accounts, and the loser is a live account somebody else can sign into.

A lost race re-reads to find the winner, so it produces a definite answer instead of an error the caller has to guess about. And the user record is only created after the claim succeeds, so a lost race cannot leave an account nobody will ever sign into.

Telegram is not OAuth, and that matters

Telegram does not redirect with a code. It hands your page a set of fields and an HMAC over them, keyed by a hash of your bot token. The signature is the entire authentication — there is no token exchange to fall back on — so a mistake here accepts anyone's claim to be any Telegram user.

Three things have to be right, and the third is the one that is usually missed: verify the HMAC in constant time, exclude the hash field itself from the signed string, and enforce freshness on the auth date. A valid signature is valid forever, so without a freshness window a captured payload is a permanent login.

What this does not do

Not a disclaimer. A page with no limits section is marketing, and the reader here is deciding whether to build against us.

  • It is authentication, not authorisation. Who someone is, not what they may do — your permission model stays yours.
  • Providers change their APIs, deprecate scopes and alter what they return. An integration is a maintenance commitment, not a one-off.
  • A user who signs in with a provider and later loses that provider account loses that route in. Offer a second method before they need it, not after.
  • Social sign-in does not replace a second factor. It moves the first factor to somebody else, which is a different property from strengthening it.
  • Not available yet. The core is built and tested; the routes are not written.

Questions

Which providers are supported?

GitHub, X (Twitter), Facebook and Telegram. Doing OAuth server-side rather than through a hosted auth provider means no console configuration is required for each one, and it is the only option for Telegram, which is not an OAuth provider at all.

Why not just use Firebase Auth or Auth0?

You can, and for many products you should. This exists because each provider has to be enabled and configured in a console, because Telegram is not supported by any of them, and because the account-linking behaviour you get is theirs rather than yours — and linking is exactly where the security decisions live.

What is PKCE and do I need it?

Proof Key for Code Exchange: the client generates a random verifier, sends only its SHA-256 hash when starting the flow, and presents the original when exchanging the code. It stops an intercepted authorization code being redeemed by anyone who did not start the flow. X requires it; GitHub supports it; there is no reason to skip it where it is available.

What happens if a user signs in with a second provider?

If the provider returns a verified email matching an existing account, the identity is linked to it. If it does not — X and Facebook do not — they get a new account, and can link the provider afterwards from settings while signed into the original. That is deliberately more steps than matching on an unverified email, which is account takeover with extra steps.

Can someone take over my account by registering my email with a provider?

No. An unverified email never links to an existing account. Linking is only ever performed against a session the user has already authenticated, so there is no path where a claim alone inherits somebody's data.

How is a Telegram login verified?

By checking the HMAC-SHA256 over the payload fields, keyed by SHA-256 of the bot token, in constant time — with the hash field excluded from the signed string, and the auth date checked for freshness in both directions. Without a freshness window a captured payload would be a permanent login; without excluding the hash, nothing verifies at all.

Does this store my users' passwords?

There are no passwords in a federated sign-in — that is the point. The provider authenticates the user and we record which provider identity maps to which account. No credential of theirs is held.

Is this available now?

Not yet. The OAuth core and the identity model are built and tested; the routes that expose them are not written. This page describes what will ship rather than something you can switch on today.

Related