LaunchKit

Social sign-in with OmniAuth

Ten providers behind one registry, plus the three account-linking cases nobody warns you about.

"Sign in with Google" looks like a morning's work. The OAuth exchange genuinely is, because OmniAuth does it for you. What takes the time is everything that happens after the provider hands you back an email address.

What OmniAuth actually gives you

Two phases. The request phase at /auth/:provider redirects the visitor to the provider. The callback phase at /auth/:provider/callback receives them back, and by the time your controller runs, OmniAuth has already validated the exchange and populated request.env["omniauth.auth"].

That payload is all you get, and all you need. Everything below is about what you do with it.

One security note first, because it is easy to miss and hard to notice: the request phase must be a POST. A <a href="/auth/google_oauth2"> link is a login CSRF waiting to happen. The omniauth-rails_csrf_protection gem forces the POST and checks the authenticity token. Plenty of tutorials still show the link version, which works, which is the problem.

Mounting providers by configuration

providers = Rails.env.test? ? Auth::OauthProvider.all : Auth::OauthProvider.configured
providers.each { |oauth_provider| provider(*oauth_provider.omniauth_args) }

Outside test, a provider mounts exactly when its credentials are filled in, and is silently absent otherwise. That is what lets a fresh install boot with no keys at all and still show a working sign-in page. In test, every provider whose gem is present is mounted even with blank credentials, because a route has to exist for a request spec to hit it.

The part that takes the time

Once the provider says "this is alice@example.com, and I have verified it", you have to decide what that means. There are three cases and they are genuinely different:

  1. The identity is already linked. Sign that user in. The lookup is on the provider's uid, never the email, or a user who changes their Google address silently loses their account.
  2. No identity, but an account exists with that email. Link them. This is safe because the provider verified the address, and unsafe the moment that stops being true.
  3. Neither. Create the account, pre-confirmed, with a strong random password the user can replace later through the forgot-password flow.

Case 2 is where products get this wrong in both directions. Refuse to link and you create a duplicate account for someone who already has one. Link on an unverified email and you have handed an attacker anyone's account.

And there is a fourth case that only exists because this product sells before it signs anyone up: a buyer claiming a paid account with a social login must be attached to the account holding the payment, not to whatever account the email happens to match.

Where the framework helps

Very little of this is OmniAuth's problem. The pieces that carry the weight are ordinary Rails: a uniqueness: { scope: :provider } validation on the identity so two providers can share a uid, find_or_initialize_by for the create-or-link branch, has_secure_password insisting on a password even for a user who will never type one, and the session concern that turns the resolved user into a signed cookie.

The articles below take each of those in turn.

Articles on this topic