Social sign-in with OmniAuth
Ten providers behind one registry, plus the three account-linking cases nobody warns you about.
Articles on this topic
-
Running ten OAuth providers without ten code paths
A provider registry, credentials-driven mounting, the CSRF trap in the request phase, and letting one user link several logins to the same account.
-
OAuth account linking: the three cases, and the one that bites
What to do when a provider hands you an email that already has an account, why the verified flag is the whole safety property, and the fourth case that appears if you sell before you sign anyone up.
-
Add Google sign-in to Rails 8, from scratch
Getting the Client ID out of Google Cloud, then wiring OmniAuth into a plain Rails 8 app: the gems, the initializer, the POST button, the callback and the identity table.
"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/:providerredirects the visitor to the provider. The callback phase at/auth/:provider/callbackreceives them back, and by the time your controller runs, OmniAuth has already validated the exchange and populatedrequest.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. Theomniauth-rails_csrf_protectiongem 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
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:
uid, never the email, or a user who changes their Google address silently loses their account.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_byfor the create-or-link branch,has_secure_passwordinsisting 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.