Social sign-in with OmniAuth
Ten providers behind one registry, plus the three account-linking cases nobody warns you about.
Articles on this topic
-
redirect_uri_mismatch: the OAuth callback URL contract
The provider is comparing two strings and they differ by a character. How OmniAuth builds the callback URL it sends, the four ways that string goes wrong, and the one line that stops deriving it from the request.
-
OmniAuth's CSRF protection: why the request phase is a POST
OmniAuth 2.0 stopped accepting GET on its own routes, so the sign-in link that worked for years now falls through to a 404. What the omniauth-rails_csrf_protection gem adds, why button_to replaces link_to, and the attack the change exists to close.
-
Sign in with Apple in Rails: the provider with no client secret
Apple gives you a Services ID, a Team ID, a Key ID and a .p8 signing key instead of a client secret, then posts the callback back cross-site. What omniauth-apple does with all of it, and what Rails has to do to receive it.
-
Testing OmniAuth in Rails, without touching the provider
OmniAuth test mode short-circuits the handshake so a callback spec is a plain get. The traps are the POST-only request phase, the mock that leaks between examples, and the route that does not exist without credentials.
-
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.
If you have not wired one up before, adding Google sign-in from scratch is the whole path, from an empty Google Cloud console to a working button.
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 registry is what keeps ten providers from becoming ten code paths. 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, which is the first thing testing OmniAuth without touching the provider has to get right.
Apple is the provider that tests that arrangement hardest, because it stores no client secret to fill in at all: Sign in with Apple takes a Services ID and a signing key instead, and the strategy mints a fresh JWT secret on every request.
The account-linking cases, and the one that bites
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. Account linking takes the three cases apart one by one.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.
The plain Rails pieces that carry social sign-in
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.
One of them is not a Rails piece at all but a gate in front of the whole flow. OmniAuth's CSRF protection covers the request phase becoming POST-only in OmniAuth 2.0: why the sign-in link turns into a routing failure, which of the two defences ships in OmniAuth itself, and the login-CSRF attack the change exists to close.
The other error that stops the flow before your code runs is a string comparison. redirect_uri_mismatch works through how OmniAuth builds the callback URL it sends, why that value is derived from the incoming request rather than from your configuration, and the four ways the string quietly differs from the one registered with the provider.