|
| 1 | +# OpenID Connect (OIDC) with ruby-oauth/oauth2 |
| 2 | + |
| 3 | +## OIDC Libraries |
| 4 | + |
| 5 | +Libraries built on top of the oauth2 gem that implement OIDC. |
| 6 | + |
| 7 | +- [gamora](https://github.com/amco/gamora-rb) - OpenID Connect Relying Party for Rails apps |
| 8 | +- [omniauth-doximity-oauth2](https://github.com/doximity/omniauth-doximity-oauth2) - OmniAuth strategy for Doximity, supporting OIDC, and using PKCE |
| 9 | +- [omniauth-himari](https://github.com/sorah/himari) - OmniAuth strategy to act as OIDC RP and use [Himari](https://github.com/sorah/himari) for OP |
| 10 | +- [omniauth-mit-oauth2](https://github.com/MITLibraries/omniauth-mit-oauth2) - OmniAuth strategy for MIT OIDC |
| 11 | + |
| 12 | +If any other libraries would like to be added to this list, please open an issue or pull request. |
| 13 | + |
| 14 | +## Raw OIDC with ruby-oauth/oauth2 |
| 15 | + |
| 16 | +This document complements the inline documentation by focusing on OpenID Connect (OIDC) 1.0 usage patterns when using this gem as an OAuth 2.0 client library. |
| 17 | + |
| 18 | +Scope of this document |
| 19 | +- Audience: Developers building an OAuth 2.0/OIDC Relying Party (RP, aka client) in Ruby. |
| 20 | +- Non-goals: This gem does not implement an OIDC Provider (OP, aka Authorization Server); for OP/server see other projects (e.g., doorkeeper + oidc extensions). |
| 21 | +- Status: Informational documentation with links to normative specs. The gem intentionally remains protocol-agnostic beyond OAuth 2.0; OIDC specifics (like ID Token validation) must be handled by your application. |
| 22 | + |
| 23 | +Key concepts refresher |
| 24 | +- OAuth 2.0 delegates authorization; it does not define authentication of the end-user. |
| 25 | +- OIDC layers an identity layer on top of OAuth 2.0, introducing: |
| 26 | + - ID Token: a JWT carrying claims about the authenticated end-user and the authentication event. |
| 27 | + - Standardized scopes: openid (mandatory), profile, email, address, phone, offline_access, and others. |
| 28 | + - UserInfo endpoint: a protected resource for retrieving user profile claims. |
| 29 | + - Discovery and Dynamic Client Registration (optional for providers/clients that support them). |
| 30 | + |
| 31 | +What this gem provides for OIDC |
| 32 | +- All OAuth 2.0 client capabilities required for OIDC flows: building authorization requests, exchanging authorization codes, refreshing tokens, and making authenticated resource requests. |
| 33 | +- Transport and parsing conveniences (snaky hash, Faraday integration, error handling, etc.). |
| 34 | +- Optional client authentication schemes useful with OIDC deployments: |
| 35 | + - basic_auth (default) |
| 36 | + - request_body (legacy) |
| 37 | + - tls_client_auth (MTLS) |
| 38 | + - private_key_jwt (OIDC-compliant when configured per OP requirements) |
| 39 | + |
| 40 | +What you must add in your app for OIDC |
| 41 | +- ID Token validation: This gem surfaces id_token values but does not verify them. Your app should: |
| 42 | + 1) Parse the JWT (header, payload, signature) |
| 43 | + 2) Fetch the OP JSON Web Key Set (JWKS) from discovery (or configure statically) |
| 44 | + 3) Select the correct key by kid (when present) and verify the signature and algorithm |
| 45 | + 4) Validate standard claims (iss, aud, exp, iat, nbf, azp, nonce when used, at_hash/c_hash when applicable) |
| 46 | + 5) Enforce expected client_id, issuer, and clock skew policies |
| 47 | +- Nonce handling for Authorization Code flow with OIDC: generate a cryptographically-random nonce, bind it to the user session before redirect, include it in authorize request, and verify it in the ID Token on return. |
| 48 | +- PKCE is best practice and often required by OPs: generate/verifier, send challenge in authorize, send verifier in token request. |
| 49 | +- Session/state management: continue to validate state to mitigate CSRF; use exact redirect_uri matching. |
| 50 | + |
| 51 | +Minimal OIDC Authorization Code example |
| 52 | + |
| 53 | +```ruby |
| 54 | +require "oauth2" |
| 55 | +require "jwt" # jwt/ruby-jwt |
| 56 | +require "net/http" |
| 57 | +require "json" |
| 58 | + |
| 59 | +client = OAuth2::Client.new( |
| 60 | + ENV.fetch("OIDC_CLIENT_ID"), |
| 61 | + ENV.fetch("OIDC_CLIENT_SECRET"), |
| 62 | + site: ENV.fetch("OIDC_ISSUER"), # e.g. https://accounts.example.com |
| 63 | + authorize_url: "/authorize", # or discovered |
| 64 | + token_url: "/token", # or discovered |
| 65 | +) |
| 66 | + |
| 67 | +# Step 1: Redirect to OP for consent/auth |
| 68 | +state = SecureRandom.hex(16) |
| 69 | +nonce = SecureRandom.hex(16) |
| 70 | +pkce_verifier = SecureRandom.urlsafe_base64(64) |
| 71 | +pkce_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(pkce_verifier)).delete("=") |
| 72 | + |
| 73 | +authz_url = client.auth_code.authorize_url( |
| 74 | + scope: "openid profile email", |
| 75 | + state: state, |
| 76 | + nonce: nonce, |
| 77 | + code_challenge: pkce_challenge, |
| 78 | + code_challenge_method: "S256", |
| 79 | + redirect_uri: ENV.fetch("OIDC_REDIRECT_URI"), |
| 80 | +) |
| 81 | +# redirect_to authz_url |
| 82 | + |
| 83 | +# Step 2: Handle callback |
| 84 | +# params[:code], params[:state] |
| 85 | +raise "state mismatch" unless params[:state] == state |
| 86 | + |
| 87 | +token = client.auth_code.get_token( |
| 88 | + params[:code], |
| 89 | + redirect_uri: ENV.fetch("OIDC_REDIRECT_URI"), |
| 90 | + code_verifier: pkce_verifier, |
| 91 | +) |
| 92 | + |
| 93 | +# The token may include: access_token, id_token, refresh_token, etc. |
| 94 | +id_token = token.params["id_token"] || token.params[:id_token] |
| 95 | + |
| 96 | +# Step 3: Validate the ID Token (simplified – add your own checks!) |
| 97 | +# Discover keys (example using .well-known) |
| 98 | +issuer = ENV.fetch("OIDC_ISSUER") |
| 99 | +jwks_uri = JSON.parse(Net::HTTP.get(URI.join(issuer, "/.well-known/openid-configuration"))). |
| 100 | + fetch("jwks_uri") |
| 101 | +jwks = JSON.parse(Net::HTTP.get(URI(jwks_uri))) |
| 102 | +keys = jwks.fetch("keys") |
| 103 | + |
| 104 | +# Use ruby-jwt JWK loader |
| 105 | +jwk_set = JWT::JWK::Set.new(keys.map { |k| JWT::JWK.import(k) }) |
| 106 | + |
| 107 | +decoded, headers = JWT.decode( |
| 108 | + id_token, |
| 109 | + nil, |
| 110 | + true, |
| 111 | + algorithms: ["RS256", "ES256", "PS256"], |
| 112 | + jwks: jwk_set, |
| 113 | + verify_iss: true, |
| 114 | + iss: issuer, |
| 115 | + verify_aud: true, |
| 116 | + aud: ENV.fetch("OIDC_CLIENT_ID"), |
| 117 | +) |
| 118 | + |
| 119 | +# Verify nonce |
| 120 | +raise "nonce mismatch" unless decoded["nonce"] == nonce |
| 121 | + |
| 122 | +# Optionally: call UserInfo |
| 123 | +userinfo = token.get("/userinfo").parsed |
| 124 | +``` |
| 125 | + |
| 126 | +Notes on discovery and registration |
| 127 | +- Discovery: Most OPs publish configuration at {issuer}/.well-known/openid-configuration (OIDC Discovery 1.0). From there, resolve authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint, etc. |
| 128 | +- Dynamic Client Registration: Some OPs allow registering clients programmatically (OIDC Dynamic Client Registration 1.0). This gem does not implement registration; use a plain HTTP client or Faraday and store credentials securely. |
| 129 | + |
| 130 | +Common pitfalls and tips |
| 131 | +- Always request the openid scope when you expect an ID Token. Without it, the OP may behave as vanilla OAuth 2.0. |
| 132 | +- Validate ID Token signature and claims before trusting any identity data. Do not rely solely on the presence of an id_token field. |
| 133 | +- Prefer Authorization Code + PKCE. Avoid Implicit; it is discouraged in modern guidance and may be disabled by providers. |
| 134 | +- Use exact redirect_uri matching, and keep your allow-list short. |
| 135 | +- For public clients that use refresh tokens, prefer sender-constrained tokens (DPoP/MTLS) or rotation with one-time-use refresh tokens, per modern best practices. |
| 136 | +- When using private_key_jwt, ensure the "aud" (or token_url) and "iss/sub" claims are set per the OP’s rules, and include kid in the JWT header when required so the OP can select the right key. |
| 137 | + |
| 138 | +Relevant specifications and references |
| 139 | +- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html |
| 140 | +- OIDC Core (final): https://openid.net/specs/openid-connect-core-1_0-final.html |
| 141 | +- How OIDC works: https://openid.net/developers/how-connect-works/ |
| 142 | +- OpenID Connect home: https://openid.net/connect/ |
| 143 | +- OIDC Discovery 1.0: https://openid.net/specs/openid-connect-discovery-1_0.html |
| 144 | +- OIDC Dynamic Client Registration 1.0: https://openid.net/specs/openid-connect-registration-1_0.html |
| 145 | +- OIDC Session Management 1.0: https://openid.net/specs/openid-connect-session-1_0.html |
| 146 | +- OIDC RP-Initiated Logout 1.0: https://openid.net/specs/openid-connect-rpinitiated-1_0.html |
| 147 | +- OIDC Back-Channel Logout 1.0: https://openid.net/specs/openid-connect-backchannel-1_0.html |
| 148 | +- OIDC Front-Channel Logout 1.0: https://openid.net/specs/openid-connect-frontchannel-1_0.html |
| 149 | +- Auth0 OIDC overview: https://auth0.com/docs/authenticate/protocols/openid-connect-protocol |
| 150 | +- Spring Authorization Server’s list of OAuth2/OIDC specs: https://github.com/spring-projects/spring-authorization-server/wiki/OAuth2-and-OIDC-Specifications |
| 151 | + |
| 152 | +See also |
| 153 | +- README sections on OAuth 2.1 notes and OIDC notes |
| 154 | +- Strategy classes under lib/oauth2/strategy for flow helpers |
| 155 | +- Specs under spec/oauth2 for concrete usage patterns |
| 156 | + |
| 157 | +Contributions welcome |
| 158 | +- If you discover provider-specific nuances, consider contributing examples or clarifications (without embedding provider-specific hacks into the library). |
0 commit comments