diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c36d47d..4882ff50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - avoid bearer tokens in query, - refresh token guidance for public clients, - simplified client definitions) +- document how to implement an OIDC client with this gem in OIDC.md + - also, list libraries built on top of the oauth2 gem that implement OIDC ### Changed ### Deprecated ### Removed @@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [gh660][gh660]- (more) Comprehensive documentation / examples by @pboling - [gh657][gh657] - Updated documentation for org-rename by @pboling - More funding links by @Aboling0 +- Documentation: Added docs/OIDC.md with OIDC 1.0 overview, example, and references ### Changed - Upgrade Code of Conduct to Contributor Covenant 2.1 by @pboling - [gh660][gh660] - Shrink post-install message by 4 lines by @pboling diff --git a/OIDC.md b/OIDC.md new file mode 100644 index 00000000..2bd7c708 --- /dev/null +++ b/OIDC.md @@ -0,0 +1,158 @@ +# OpenID Connect (OIDC) with ruby-oauth/oauth2 + +## OIDC Libraries + +Libraries built on top of the oauth2 gem that implement OIDC. + +- [gamora](https://github.com/amco/gamora-rb) - OpenID Connect Relying Party for Rails apps +- [omniauth-doximity-oauth2](https://github.com/doximity/omniauth-doximity-oauth2) - OmniAuth strategy for Doximity, supporting OIDC, and using PKCE +- [omniauth-himari](https://github.com/sorah/himari) - OmniAuth strategy to act as OIDC RP and use [Himari](https://github.com/sorah/himari) for OP +- [omniauth-mit-oauth2](https://github.com/MITLibraries/omniauth-mit-oauth2) - OmniAuth strategy for MIT OIDC + +If any other libraries would like to be added to this list, please open an issue or pull request. + +## Raw OIDC with ruby-oauth/oauth2 + +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. + +Scope of this document +- Audience: Developers building an OAuth 2.0/OIDC Relying Party (RP, aka client) in Ruby. +- 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). +- 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. + +Key concepts refresher +- OAuth 2.0 delegates authorization; it does not define authentication of the end-user. +- OIDC layers an identity layer on top of OAuth 2.0, introducing: + - ID Token: a JWT carrying claims about the authenticated end-user and the authentication event. + - Standardized scopes: openid (mandatory), profile, email, address, phone, offline_access, and others. + - UserInfo endpoint: a protected resource for retrieving user profile claims. + - Discovery and Dynamic Client Registration (optional for providers/clients that support them). + +What this gem provides for OIDC +- All OAuth 2.0 client capabilities required for OIDC flows: building authorization requests, exchanging authorization codes, refreshing tokens, and making authenticated resource requests. +- Transport and parsing conveniences (snaky hash, Faraday integration, error handling, etc.). +- Optional client authentication schemes useful with OIDC deployments: + - basic_auth (default) + - request_body (legacy) + - tls_client_auth (MTLS) + - private_key_jwt (OIDC-compliant when configured per OP requirements) + +What you must add in your app for OIDC +- ID Token validation: This gem surfaces id_token values but does not verify them. Your app should: + 1) Parse the JWT (header, payload, signature) + 2) Fetch the OP JSON Web Key Set (JWKS) from discovery (or configure statically) + 3) Select the correct key by kid (when present) and verify the signature and algorithm + 4) Validate standard claims (iss, aud, exp, iat, nbf, azp, nonce when used, at_hash/c_hash when applicable) + 5) Enforce expected client_id, issuer, and clock skew policies +- 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. +- PKCE is best practice and often required by OPs: generate/verifier, send challenge in authorize, send verifier in token request. +- Session/state management: continue to validate state to mitigate CSRF; use exact redirect_uri matching. + +Minimal OIDC Authorization Code example + +```ruby +require "oauth2" +require "jwt" # jwt/ruby-jwt +require "net/http" +require "json" + +client = OAuth2::Client.new( + ENV.fetch("OIDC_CLIENT_ID"), + ENV.fetch("OIDC_CLIENT_SECRET"), + site: ENV.fetch("OIDC_ISSUER"), # e.g. https://accounts.example.com + authorize_url: "/authorize", # or discovered + token_url: "/token", # or discovered +) + +# Step 1: Redirect to OP for consent/auth +state = SecureRandom.hex(16) +nonce = SecureRandom.hex(16) +pkce_verifier = SecureRandom.urlsafe_base64(64) +pkce_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(pkce_verifier)).delete("=") + +authz_url = client.auth_code.authorize_url( + scope: "openid profile email", + state: state, + nonce: nonce, + code_challenge: pkce_challenge, + code_challenge_method: "S256", + redirect_uri: ENV.fetch("OIDC_REDIRECT_URI"), +) +# redirect_to authz_url + +# Step 2: Handle callback +# params[:code], params[:state] +raise "state mismatch" unless params[:state] == state + +token = client.auth_code.get_token( + params[:code], + redirect_uri: ENV.fetch("OIDC_REDIRECT_URI"), + code_verifier: pkce_verifier, +) + +# The token may include: access_token, id_token, refresh_token, etc. +id_token = token.params["id_token"] || token.params[:id_token] + +# Step 3: Validate the ID Token (simplified – add your own checks!) +# Discover keys (example using .well-known) +issuer = ENV.fetch("OIDC_ISSUER") +jwks_uri = JSON.parse(Net::HTTP.get(URI.join(issuer, "/.well-known/openid-configuration"))). + fetch("jwks_uri") +jwks = JSON.parse(Net::HTTP.get(URI(jwks_uri))) +keys = jwks.fetch("keys") + +# Use ruby-jwt JWK loader +jwk_set = JWT::JWK::Set.new(keys.map { |k| JWT::JWK.import(k) }) + +decoded, headers = JWT.decode( + id_token, + nil, + true, + algorithms: ["RS256", "ES256", "PS256"], + jwks: jwk_set, + verify_iss: true, + iss: issuer, + verify_aud: true, + aud: ENV.fetch("OIDC_CLIENT_ID"), +) + +# Verify nonce +raise "nonce mismatch" unless decoded["nonce"] == nonce + +# Optionally: call UserInfo +userinfo = token.get("/userinfo").parsed +``` + +Notes on discovery and registration +- 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. +- 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. + +Common pitfalls and tips +- Always request the openid scope when you expect an ID Token. Without it, the OP may behave as vanilla OAuth 2.0. +- Validate ID Token signature and claims before trusting any identity data. Do not rely solely on the presence of an id_token field. +- Prefer Authorization Code + PKCE. Avoid Implicit; it is discouraged in modern guidance and may be disabled by providers. +- Use exact redirect_uri matching, and keep your allow-list short. +- 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. +- 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. + +Relevant specifications and references +- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html +- OIDC Core (final): https://openid.net/specs/openid-connect-core-1_0-final.html +- How OIDC works: https://openid.net/developers/how-connect-works/ +- OpenID Connect home: https://openid.net/connect/ +- OIDC Discovery 1.0: https://openid.net/specs/openid-connect-discovery-1_0.html +- OIDC Dynamic Client Registration 1.0: https://openid.net/specs/openid-connect-registration-1_0.html +- OIDC Session Management 1.0: https://openid.net/specs/openid-connect-session-1_0.html +- OIDC RP-Initiated Logout 1.0: https://openid.net/specs/openid-connect-rpinitiated-1_0.html +- OIDC Back-Channel Logout 1.0: https://openid.net/specs/openid-connect-backchannel-1_0.html +- OIDC Front-Channel Logout 1.0: https://openid.net/specs/openid-connect-frontchannel-1_0.html +- Auth0 OIDC overview: https://auth0.com/docs/authenticate/protocols/openid-connect-protocol +- Spring Authorization Server’s list of OAuth2/OIDC specs: https://github.com/spring-projects/spring-authorization-server/wiki/OAuth2-and-OIDC-Specifications + +See also +- README sections on OAuth 2.1 notes and OIDC notes +- Strategy classes under lib/oauth2/strategy for flow helpers +- Specs under spec/oauth2 for concrete usage patterns + +Contributions welcome +- If you discover provider-specific nuances, consider contributing examples or clarifications (without embedding provider-specific hacks into the library). diff --git a/README.md b/README.md index 56365fa0..434f932c 100644 --- a/README.md +++ b/README.md @@ -947,6 +947,7 @@ access = client.get_token({ - If the token response includes an `id_token` (a JWT), this gem surfaces it but does not validate the signature. Use a JWT library and your provider's JWKs to verify it. - For private_key_jwt client authentication, provide `auth_scheme: :private_key_jwt` and ensure your key configuration matches the provider requirements. +- See [OIDC.md](OIDC.md) for a more complete OIDC overview, example, and links to the relevant specifications. ### Debugging diff --git a/docs/OAuth2.html b/docs/OAuth2.html index f147de49..29589692 100644 --- a/docs/OAuth2.html +++ b/docs/OAuth2.html @@ -415,7 +415,7 @@

diff --git a/docs/OAuth2/AccessToken.html b/docs/OAuth2/AccessToken.html index 9390625c..40443e45 100644 --- a/docs/OAuth2/AccessToken.html +++ b/docs/OAuth2/AccessToken.html @@ -3069,7 +3069,7 @@

diff --git a/docs/OAuth2/Authenticator.html b/docs/OAuth2/Authenticator.html index dabcdf70..2d8d9d58 100644 --- a/docs/OAuth2/Authenticator.html +++ b/docs/OAuth2/Authenticator.html @@ -883,7 +883,7 @@

diff --git a/docs/OAuth2/Client.html b/docs/OAuth2/Client.html index f7320c23..eb046f10 100644 --- a/docs/OAuth2/Client.html +++ b/docs/OAuth2/Client.html @@ -2656,7 +2656,7 @@

diff --git a/docs/OAuth2/Error.html b/docs/OAuth2/Error.html index 565b17d6..b852be13 100644 --- a/docs/OAuth2/Error.html +++ b/docs/OAuth2/Error.html @@ -772,7 +772,7 @@

diff --git a/docs/OAuth2/FilteredAttributes.html b/docs/OAuth2/FilteredAttributes.html index 8add3c57..82ca2b5c 100644 --- a/docs/OAuth2/FilteredAttributes.html +++ b/docs/OAuth2/FilteredAttributes.html @@ -335,7 +335,7 @@

diff --git a/docs/OAuth2/FilteredAttributes/ClassMethods.html b/docs/OAuth2/FilteredAttributes/ClassMethods.html index 53511059..e729ba40 100644 --- a/docs/OAuth2/FilteredAttributes/ClassMethods.html +++ b/docs/OAuth2/FilteredAttributes/ClassMethods.html @@ -280,7 +280,7 @@

diff --git a/docs/OAuth2/Response.html b/docs/OAuth2/Response.html index a9fb6698..17bbd4f8 100644 --- a/docs/OAuth2/Response.html +++ b/docs/OAuth2/Response.html @@ -1619,7 +1619,7 @@

diff --git a/docs/OAuth2/Strategy.html b/docs/OAuth2/Strategy.html index 14eeec5c..1117d94d 100644 --- a/docs/OAuth2/Strategy.html +++ b/docs/OAuth2/Strategy.html @@ -107,7 +107,7 @@

Defined Under Namespace

diff --git a/docs/OAuth2/Strategy/Assertion.html b/docs/OAuth2/Strategy/Assertion.html index 76a454a4..69264be5 100644 --- a/docs/OAuth2/Strategy/Assertion.html +++ b/docs/OAuth2/Strategy/Assertion.html @@ -481,7 +481,7 @@

diff --git a/docs/OAuth2/Strategy/AuthCode.html b/docs/OAuth2/Strategy/AuthCode.html index 73881c8e..6480e47e 100644 --- a/docs/OAuth2/Strategy/AuthCode.html +++ b/docs/OAuth2/Strategy/AuthCode.html @@ -483,7 +483,7 @@

diff --git a/docs/OAuth2/Strategy/Base.html b/docs/OAuth2/Strategy/Base.html index ed53e472..05d29d90 100644 --- a/docs/OAuth2/Strategy/Base.html +++ b/docs/OAuth2/Strategy/Base.html @@ -195,7 +195,7 @@

diff --git a/docs/OAuth2/Strategy/ClientCredentials.html b/docs/OAuth2/Strategy/ClientCredentials.html index 140ac362..1e935d28 100644 --- a/docs/OAuth2/Strategy/ClientCredentials.html +++ b/docs/OAuth2/Strategy/ClientCredentials.html @@ -343,7 +343,7 @@

diff --git a/docs/OAuth2/Strategy/Implicit.html b/docs/OAuth2/Strategy/Implicit.html index 7db5f9e2..f6a52a17 100644 --- a/docs/OAuth2/Strategy/Implicit.html +++ b/docs/OAuth2/Strategy/Implicit.html @@ -420,7 +420,7 @@

diff --git a/docs/OAuth2/Strategy/Password.html b/docs/OAuth2/Strategy/Password.html index 1db42065..874eacd3 100644 --- a/docs/OAuth2/Strategy/Password.html +++ b/docs/OAuth2/Strategy/Password.html @@ -374,7 +374,7 @@

diff --git a/docs/OAuth2/Version.html b/docs/OAuth2/Version.html index 1fa26949..185cbff7 100644 --- a/docs/OAuth2/Version.html +++ b/docs/OAuth2/Version.html @@ -111,7 +111,7 @@

diff --git a/docs/_index.html b/docs/_index.html index 48ac3aaa..0aeb681f 100644 --- a/docs/_index.html +++ b/docs/_index.html @@ -75,70 +75,73 @@

File Listing

  • FUNDING
  • -
  • RUBOCOP
  • +
  • OIDC
  • -
  • SECURITY
  • +
  • RUBOCOP
  • -
  • LICENSE
  • +
  • SECURITY
  • -
  • CITATION
  • +
  • LICENSE
  • -
  • oauth2-2.0.10.gem
  • +
  • CITATION
  • -
  • oauth2-2.0.11.gem
  • +
  • oauth2-2.0.10.gem
  • -
  • oauth2-2.0.12.gem
  • +
  • oauth2-2.0.11.gem
  • -
  • oauth2-2.0.13.gem
  • +
  • oauth2-2.0.12.gem
  • -
  • oauth2-2.0.10.gem
  • +
  • oauth2-2.0.13.gem
  • -
  • oauth2-2.0.11.gem
  • +
  • oauth2-2.0.10.gem
  • -
  • oauth2-2.0.12.gem
  • +
  • oauth2-2.0.11.gem
  • -
  • oauth2-2.0.13.gem
  • +
  • oauth2-2.0.12.gem
  • -
  • REEK
  • +
  • oauth2-2.0.13.gem
  • -
  • access_token
  • +
  • REEK
  • -
  • authenticator
  • +
  • access_token
  • -
  • client
  • +
  • authenticator
  • -
  • error
  • +
  • client
  • -
  • filtered_attributes
  • +
  • error
  • -
  • response
  • +
  • filtered_attributes
  • -
  • strategy
  • +
  • response
  • -
  • version
  • +
  • strategy
  • -
  • oauth2
  • +
  • version
  • + + +
  • oauth2
  • @@ -363,7 +366,7 @@

    Namespace Listing A-Z

    diff --git a/docs/file.CHANGELOG.html b/docs/file.CHANGELOG.html index d8e4431a..1800d34a 100644 --- a/docs/file.CHANGELOG.html +++ b/docs/file.CHANGELOG.html @@ -74,7 +74,12 @@

    Added

  • implicit/password grants omitted,
  • avoid bearer tokens in query,
  • refresh token guidance for public clients,
  • -
  • simplified client definitions) +
  • simplified client definitions)
  • + + +
  • document how to implement an OIDC client with this gem in OIDC.md +