Adding OpenID Connect Login to Rails Without the Dependency Pile
“Sign in with…” looks like a solved problem until you open the box and find five HTTP clients and a transitive dependency graph the size of a small moon. Here is how OpenID Connect actually works, and two small Ruby gems that do it with almost nothing.
Every app eventually needs “log in with your work account,” and OpenID Connect (OIDC) is how that usually happens. It is a thin, standard layer on top of OAuth2 that adds one crucial thing OAuth2 left out: a straight answer to “who is this user?”
The reputation of OIDC is that it is fiddly, and the usual fix is to install a library that installs a library that installs an HTTP client you have never heard of. I went the other way. I wrote two small gems that do OIDC login with only the dependencies you can name: omniauth_oidc for the login flow, and openid_config_parser for the discovery step. This post is what I learned about the protocol along the way, because once you see the shape of it, the fear goes away.
OIDC in one picture
Strip away the jargon and OIDC login is four moves:
1. Your app sends the user to the provider (Google, Microsoft Entra, Okta, whoever) to log in.
2. The provider sends the user back with a short-lived code.
3. Your app trades that code for tokens, including an ID token.
4. The ID token is a signed statement that says “this is who they are.” Your app checks the signature and the claims inside it, and only then believes it.
That is the whole thing. The reason it feels hard is the ceremony around each step: how does your app know the provider’s URLs, how does it check the signature, how does it stop a stolen code from being replayed. Each of those has a clean answer, and two of them are the gems below.
Step zero: discovery, or how to stop hardcoding URLs
Every OIDC provider publishes a little JSON file at a well-known address, `/.well-known/openid-configuration`. It lists everything your app needs: where to send the user, where to trade the code, where to fetch the signing keys.
Hardcoding those URLs is how you wake up to a broken login the day the provider moves one.
Get Suleyman Musayev’s stories in your inbox
Join Medium for free to get updates from this writer.
So the first gem, `openid_config_parser`, does exactly one job well: fetch and parse that file.
config = OpenidConfigParser.fetch_openid_configuration(
"https://example.com/.well-known/openid-configuration"
)
config.issuer # => "https://example.com"
config[:authorization_endpoint] # => "https://example.com/authorize"
config["jwks_uri"] # => "https://example.com/.well-known/jwks.json"It is zero-dependency on purpose, and the boring parts are the point: it caches the result (so you are not refetching that file on every login), retries on a timeout, validates that the response actually looks like an OIDC config, and lets you set real timeouts instead of waiting the default sixty seconds for a provider having a bad day. A discovery step that hangs is a login page that hangs. Small gem, but it sits on the critical path, so it should be calm under pressure.
The login flow, with the sharp edges filed down
The second gem, `omniauth_oidc`, is an OmniAuth strategy, so it slots into the same place every “sign in with” button in Rails already lives:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :oidc, {
name: :my_provider,
client_options: {
identifier: ENV["OIDC_CLIENT_ID"],
secret: ENV["OIDC_CLIENT_SECRET"],
config_endpoint: "https://provider.example/.well-known/openid-configuration"
}
}
endPoint it at the discovery endpoint and it wires up the rest. Under the hood it handles the four things that separate a toy from a login you can ship: PKCE, which stops an attacker who intercepts the authorization code from using it. The app sends a secret challenge on the way out and proves it on the way back, so a stolen code alone is worthless. On a public client this is not optional, and the gem does it for you.
JWKS key rotation. The provider signs ID tokens with keys it rotates on its own schedule, and publishes the current set at its `jwks_uri`. Your app has to follow along, or one day the provider rotates a key and every login fails the signature check at once. The strategy fetches and tracks those keys so you do not get paged for someone else’s key rotation.
ID token claims. A valid signature only proves the provider issued the token. It does not prove the token was meant for your app, or for this login. So the strategy also checks four claims: iss (the token came from the provider you expect), aud (it was issued for your client ID), exp (it has not expired), and nonce (it matches the value sent at the start of this login). If any of them is wrong, the token is rejected.
Both `code` and `id_token` response types, so it works with providers that do the standard code flow and the ones that hand you an ID token more directly.
The dependency diet
Here is the part I am quietly proud of: `omniauth_oidc` needs only `omniauth` and `jwt`, and every HTTP call uses Ruby’s built-in `Net::HTTP`. No Faraday, no HTTParty, no swappable middleware stack for making a handful of GET requests. A login library is security-critical code that runs on every sign-in, and every dependency it drags in is code you now trust with your authentication and code you have to keep patched. The smaller the surface, the fewer the surprises. You do not need a moving company to carry a backpack.
Takeaways
- OIDC login is four moves: send the user to the provider, get a code back, trade it for an ID token, verify the token’s signature and claims. The rest is ceremony.
- Never hardcode provider URLs. Fetch the discovery document, and cache and time-box that fetch, because it is on the login critical path.
- PKCE, JWKS key rotation, and claim checks (
iss,aud,exp,nonce) are what separate a demo from a login you can ship. Do not skip them. - A security-critical library earns its keep by being small. Fewer dependencies means fewer things to trust and patch.
Both gems are on GitHub omniauth_oidc, openid_config_parser and RubyGems. There is also an older, longer walkthrough of building the OmniAuth strategy if you want the step-by-step.
I am a backend and systems engineer. I work on authentication, PKI, and cryptography in Ruby and C, and, occasionally, the fraud and abuse that come with running them. More at msuliq.github.io.





