omniauth_oidc 0.2.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e78d766cf25ec6bae84d2af8d007cfafd53cdc1c5f67c448b91f04d80cf8bc9
4
- data.tar.gz: 572884ae38c7d8a4c89882b8609348a6bcc5aea0c4f7e7f0303e1a4e9a763136
3
+ metadata.gz: 235b9ddce05f36feb1cff322acfd22e06969de1ae49673b607fbb6779b85d448
4
+ data.tar.gz: 4cfec340b6c5cc585091829b3e4b6b8bd3db913773687481a05fc52fc3e27ea0
5
5
  SHA512:
6
- metadata.gz: d48453b68849c0ec1a34a0d58805efb3a1cecbb4966d40e0ba4098d679330cf5d7f27b401e9a6a2d6d4f4bae4dc36224bb5b0788299358e6a8054b1c35666996
7
- data.tar.gz: b7dfad42cf0c15f34d36ce0676e79da70e03d7114695578dc170fe9be2338424b0cc20dbeffe9f503820016bee65f8c5de0c6388d56d78320a48e03a2cad7ad7
6
+ metadata.gz: ebae17e00270c2aeefe3937993de033f7241d1f98cfbc630e9acd2163fbbe5011808da0189cef14fa20d9f94833b5ef014fbfc99652f6e16448048c25ee7e6fe
7
+ data.tar.gz: 6d908f0af3cd3e341f3ef3fdfa8c2f6c36fb15f4c1d94a17cb72d1ea1c28eb6f92b368696c32daf11dd89a72a78e76f409c28be3668ba42ab262677bacd9c14d
data/.rubocop.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.7
3
+ NewCops: enable
3
4
 
4
5
  Style/StringLiterals:
5
6
  Enabled: true
@@ -21,7 +22,7 @@ Metrics/MethodLength:
21
22
  Metrics/AbcSize:
22
23
  Max: 35
23
24
 
24
- Metrics/Metrics/CyclomaticComplexity:
25
+ Metrics/CyclomaticComplexity:
25
26
  Max: 10
26
27
 
27
28
  Metrics/PerceivedComplexity:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  ## [Released]
2
2
 
3
+ ## [1.0.0] - 2026-03-02
4
+
5
+ ### BREAKING CHANGES
6
+ - **Dependency Replacement**: Removed `openid_connect`, `openid_config_parser`, `json-jwt`, and `httparty` runtime dependencies. Replaced with custom `Net::HTTP`-based implementation and the standard `jwt` gem. Any code that relied on these gems being transitively available will need to add them directly.
7
+ - **Internal Object Types Changed**: Response objects are now custom classes instead of OpenIDConnect library types:
8
+ - `OpenIDConnect::Client` replaced by `OmniauthOidc::Client`
9
+ - `OpenIDConnect::ResponseObject::IdToken` replaced by `OmniauthOidc::ResponseObjects::IdToken`
10
+ - `OpenIDConnect::ResponseObject::UserInfo` replaced by `OmniauthOidc::ResponseObjects::UserInfo`
11
+ - `Rack::OAuth2::AccessToken` replaced by `OmniauthOidc::ResponseObjects::AccessToken`
12
+ - **Session Keys Namespaced**: Session keys now include the provider name (e.g. `omniauth.my_provider.state` instead of `omniauth.state`). This enables multiple OIDC providers but breaks code that manually accesses session keys with the old format. Users mid-authentication during upgrade will see state mismatch errors.
13
+ - **Scope Return Type**: `scope` now returns a space-delimited String (e.g. `"openid profile email"`) instead of an Array. Code that called `.each`, `.include?`, or other Array methods on the return value will break.
14
+ - **AuthHash Construction**: Now uses OmniAuth DSL blocks (`info`, `credentials`, `extra`) consistently instead of manually building `env["omniauth.auth"]`. The `info` hash now includes all standard OIDC UserInfo fields (given_name, family_name, gender, picture, phone, website).
15
+ - **Error Classes Restructured**: Error handling uses new specific error classes under `OmniauthOidc::` namespace. `Rack::OAuth2::Client::Error` is no longer caught. `validate_client_algorithm!` now raises `OmniauthOidc::InvalidAlgorithmError` instead of `CallbackError`.
16
+ - **CallbackError Parameter Names**: `CallbackError` now accepts `error_reason:` and `error_uri:` keyword arguments (previously `reason:` and `uri:`). The old keys are still accepted for backward compatibility.
17
+
18
+ ### Added
19
+ - **Custom HTTP Client** (`OmniauthOidc::HttpClient`): Net::HTTP-based client for all HTTP requests, removing external HTTP client dependencies entirely
20
+ - GET requests follow up to 5 redirects (301, 302, 307, 308), including relative redirects
21
+ - POST requests reject redirects to prevent credential leakage
22
+ - **Custom OIDC Client** (`OmniauthOidc::Client`): Handles authorization URI construction, token exchange, and userinfo fetching
23
+ - **Response Objects** (`OmniauthOidc::ResponseObjects`): `IdToken`, `UserInfo`, and `AccessToken` classes with method-style access to standard OIDC claims
24
+ - **Configuration Fetcher** (`OmniauthOidc::ConfigFetcher`): Fetches and parses `.well-known/openid-configuration` with retry support
25
+ - **JWK Handler** (`OmniauthOidc::JwkHandler`): JWKS parsing with proper `kid`-based key selection for providers that publish multiple signing keys (e.g. Google, Microsoft Entra ID, Auth0)
26
+ - **JWKS Caching** (`OmniauthOidc::JwksCache`): Thread-safe JWKS cache with configurable TTL (default 1 hour)
27
+ - Automatic cache invalidation and retry on signature verification failure
28
+ - Manual cache control via `OmniauthOidc::JwksCache.clear!` and `.invalidate(uri)`
29
+ - Configurable via `jwks_cache_ttl` option
30
+ - **Logging & Instrumentation** (`OmniauthOidc::Logging`): Logging via Ruby Logger with optional ActiveSupport::Notifications integration
31
+ - Configurable log levels (default: WARN)
32
+ - Automatic sanitization of sensitive data (tokens, secrets, code verifiers)
33
+ - Event instrumentation for config fetch, token exchange, userinfo fetch, JWKS fetch, request phase, and callback phase
34
+ - **Error Hierarchy**: Specific error classes for all failure modes
35
+ - `TokenVerificationError`, `TokenExpiredError`, `InvalidAlgorithmError`, `InvalidSignatureError`
36
+ - `InvalidIssuerError`, `InvalidAudienceError`, `InvalidNonceError`
37
+ - `JwksFetchError`, `KeyNotFoundError`
38
+ - `ConfigurationError`, `MissingConfigurationError`
39
+ - **Configuration Validation**: Required options (`identifier`, `secret`, `config_endpoint`) are validated on first request with a clear error message listing all missing fields
40
+ - **RBS Type Signatures** (`sig/omniauth_oidc.rbs`): Full type declarations for all public API classes
41
+ - **Comprehensive Test Suite**: Unit tests for HTTP client, JWK handler, JWKS cache, logging, errors, and strategy configuration
42
+
43
+ ### Fixed
44
+ - **JWK kid Selection**: `keyset_for_algorithm` now selects the correct signing key by `kid` from JWKS. Previously, with multiple keys in the JWKS, verification could fail or use the wrong key.
45
+ - **Dead Code Removed**: Removed `decode` method in Verify module that referenced undefined `UrlSafeBase64` constant
46
+ - **Typo**: Fixed `client_singing_alg` to `client_signing_alg` in error messages
47
+ - **Scope Default**: Fixed default scope fallback from `[:open_id]` (typo) to `[:openid]`
48
+ - **AuthHash Consistency**: Consolidated duplicate user info fetching and AuthHash construction into OmniAuth DSL blocks
49
+ - **Removed `resolve_endpoint_from_host`**: Endpoints are now used as full URLs from the discovery document instead of being stripped to relative paths
50
+
51
+ ### Changed
52
+ - **Module Loading**: Replaced `Dir.glob` with explicit `require_relative` for clarity and predictable load order
53
+ - **Dependency Constraints**: Runtime dependencies reduced to `omniauth ~> 2.1` and `jwt ~> 2.7`
54
+ - **CI Updates**: Added Ruby 4.0.1 to test matrix, updated `actions/checkout` to v6, RuboCop runs on Ruby 3.3
55
+ - **RuboCop Config**: Fixed invalid cop name `Metrics/Metrics/CyclomaticComplexity`, added `NewCops: enable`
56
+
57
+ ### Migration Guide from 0.x to 1.0.0
58
+
59
+ 1. **Dependencies**: Remove gems that were only used by this gem, then run `bundle install`:
60
+ - `httparty`
61
+ - `openid_connect`
62
+ - `openid_config_parser`
63
+ - `json-jwt`
64
+ 2. **Session Keys**: If you manually access session keys, update to the new namespaced format: `omniauth.{provider_name}.state`, `omniauth.{provider_name}.nonce`.
65
+ 3. **Scope Handling**: If your code treats `scope` as an Array, update it. `scope` now returns a space-delimited String.
66
+ 4. **Error Handling**: Update rescue clauses to catch the new error classes if needed. For example:
67
+ - `Rack::OAuth2::Client::Error` -> `OmniauthOidc::TokenError`
68
+ - Algorithm mismatches now raise `OmniauthOidc::InvalidAlgorithmError` instead of `CallbackError`
69
+ 5. **Response Objects**: If you access `access_token` or `user_info` objects directly, note they are now `OmniauthOidc::ResponseObjects::AccessToken` and `OmniauthOidc::ResponseObjects::UserInfo` respectively.
70
+ 6. **Logging** (optional): Configure logging level: `OmniauthOidc::Logging.log_level = Logger::INFO`
71
+ 7. **Ruby 4.0+**: Add `gem "ostruct"` to your Gemfile (removed from default gems in Ruby 4.0).
72
+
3
73
  ## [0.2.7] - 2024-10-11
4
74
  - Dependencies update
5
75
 
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 Suleyman Musayev
3
+ Copyright (c) 2024-present Suleyman Musayev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # OmniAuth::Oidc
2
2
 
3
- This gem provides an OmniAuth strategy for integrating OpenID Connect (OIDC) authentication into your Ruby on Rails application. It allows seamless login using various OIDC providers.
3
+ OmniAuth strategy for OpenID Connect (OIDC) authentication. Supports multiple OIDC providers, PKCE, JWKS key rotation, and both `code` and `id_token` response types.
4
+
5
+ Minimal dependencies: only `omniauth` and `jwt` gems required. All HTTP requests use Ruby's built-in `Net::HTTP` — no Faraday, HTTParty, or other external HTTP clients.
4
6
 
5
7
  Developed with reference to [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect) and [omniauth_openid_connect](https://github.dev/omniauth/omniauth_openid_connect).
6
8
 
@@ -16,14 +18,15 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
18
 
17
19
  $ gem install omniauth_oidc
18
20
 
21
+ **Ruby 4.0+**: Add `gem "ostruct"` to your Gemfile (`ostruct` was removed from default gems in Ruby 4.0).
19
22
 
20
23
  ## Usage
21
24
 
22
25
  To use the OmniAuth OIDC strategy, you need to configure your Rails application and set up the necessary environment variables for OIDC client credentials.
23
26
 
24
27
  ### Configuration
25
- You have to provide Client ID, Client Secret and url for the OIDC configuration endpoint as a bare minimum for the `omniauth_oidc` to work properly.
26
- Create an initializer file at `config/initializers/omniauth.rb`
28
+
29
+ You must provide Client ID, Client Secret, and the URL for the OIDC configuration endpoint as a minimum for `omniauth_oidc` to work. Create an initializer file at `config/initializers/omniauth.rb`:
27
30
 
28
31
  ```ruby
29
32
  # config/initializers/omniauth.rb
@@ -39,13 +42,13 @@ Rails.application.config.middleware.use OmniAuth::Builder do
39
42
  end
40
43
  ```
41
44
 
42
- With Devise
45
+ With Devise:
43
46
 
44
47
  ```ruby
45
48
  Devise.setup do |config|
46
49
  config.omniauth :oidc, {
47
50
  name: :simple_provider,
48
- scope: [:openid, :email, :profile, :address],
51
+ scope: %w[openid email profile address],
49
52
  response_type: :code,
50
53
  uid_field: "preferred_username",
51
54
  client_options: {
@@ -57,7 +60,7 @@ Devise.setup do |config|
57
60
  end
58
61
  ```
59
62
 
60
- The gem also supports a wide range of optional parameters for higher degree of configurability.
63
+ The gem also supports a wide range of optional parameters for a higher degree of configurability:
61
64
 
62
65
  ```ruby
63
66
  # config/initializers/omniauth.rb
@@ -65,7 +68,7 @@ Rails.application.config.middleware.use OmniAuth::Builder do
65
68
  provider :oidc, {
66
69
  name: :complex_provider, # used for dynamic routing
67
70
  issuer: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77',
68
- scope: [:openid],
71
+ scope: %w[openid],
69
72
  response_type: 'id_token',
70
73
  require_state: true,
71
74
  response_mode: :query,
@@ -73,24 +76,25 @@ Rails.application.config.middleware.use OmniAuth::Builder do
73
76
  send_nonce: false,
74
77
  uid_field: "sub",
75
78
  pkce: false,
79
+ jwks_cache_ttl: 3600, # JWKS cache TTL in seconds (default: 3600)
76
80
  client_options: {
77
81
  identifier: '23575f4602bebbd9a17dbc38d85bd1a77',
78
82
  secret: ENV['COMPLEX_PROVIDER_CLIENT_SECRET'],
79
83
  config_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/.well-known/openid-configuration',
80
- host: 'complexprovider.com'
84
+ host: 'complexprovider.com',
81
85
  scheme: "https",
82
86
  port: 443,
83
87
  authorization_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/authorization',
84
88
  token_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/token',
85
89
  userinfo_endpoint: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/userinfo',
86
90
  jwks_uri: 'https://complexprovider.com/cdn-cgi/access/sso/oidc/23575f4602bebbd9a17dbc38d85bd1a77/jwks',
87
- end_session_endpoint: '/signout'
91
+ end_session_endpoint: 'https://complexprovider.com/signout'
88
92
  }
89
93
  }
90
94
  end
91
95
  ```
92
96
 
93
- Ensure to replace identifier, secret, configuration endpoint url and others with credentials received from your OIDC provider.
97
+ Ensure to replace identifier, secret, configuration endpoint URL and others with credentials received from your OIDC provider.
94
98
  Please note that the gem does not accept `redirect_uri` as a configurable option. For details please see section Routes.
95
99
 
96
100
  ### Redirecting for Authentication
@@ -103,7 +107,7 @@ Buttons and links to initialize the authentication request can be placed on rele
103
107
 
104
108
  ### Handling Callbacks
105
109
 
106
- The gem uses dyanmic routes to handle different phases, and while you can use same routes in your Rails application, for
110
+ The gem uses dynamic routes to handle different phases, and while you can use same routes in your Rails application, for
107
111
  better experience you should have a controller to process the authenticated user. Create a CallbacksController:
108
112
 
109
113
  ```ruby
@@ -113,9 +117,9 @@ class CallbacksController < ApplicationController
113
117
  # user info received from OIDC provider will be available in `request.env['omniauth.auth']`
114
118
  auth = request.env['omniauth.auth']
115
119
 
116
- user = User.find_or_create_by(uid: auth['uid']) do |user|
117
- user.name = auth['info']['name']
118
- user.email = auth['info']['email']
120
+ user = User.find_or_create_by(uid: auth['uid']) do |u|
121
+ u.name = auth['info']['name']
122
+ u.email = auth['info']['email']
119
123
  end
120
124
 
121
125
  session[:user_id] = user.id
@@ -124,17 +128,48 @@ class CallbacksController < ApplicationController
124
128
  end
125
129
  ```
126
130
 
131
+ The `omniauth.auth` hash includes:
132
+
133
+ ```ruby
134
+ {
135
+ provider: :simple_provider,
136
+ uid: "user123",
137
+ info: {
138
+ name: "Test User",
139
+ email: "test@example.com",
140
+ email_verified: true,
141
+ nickname: "testuser", # preferred_username
142
+ first_name: "Test", # given_name
143
+ last_name: "User", # family_name
144
+ gender: "male",
145
+ image: "https://example.com/avatar.jpg", # picture
146
+ phone: "+1234567890", # phone_number
147
+ urls: { website: "https://testuser.com" }
148
+ },
149
+ credentials: {
150
+ id_token: "eyJ...",
151
+ token: "access_token_value",
152
+ refresh_token: "refresh_token_value",
153
+ expires_in: 3600,
154
+ scope: "openid profile email"
155
+ },
156
+ extra: {
157
+ raw_info: { ... } # full userinfo response attributes
158
+ }
159
+ }
160
+ ```
161
+
127
162
  ### Routes
128
163
 
129
164
  The gem uses dynamic routes when making requests to the OIDC provider endpoints, so called `redirect_uri` which is a
130
- non-configurable value that follows the naming pattern of `https://your_app.com/auth/<simple_provider>/callback`,
165
+ non-configurable value that follows the naming pattern of `https://your_app.com/auth/<simple_provider>/callback`,
131
166
  where `<simple_provider>` is the provider name defined within the configuration of the `omniauth.rb` initializer.
132
167
  This represents the `redirect_uri` that will be passed with the authorization request to your OIDC provider and that
133
168
  has to be registered with your OIDC provider as permitted `redirect_uri`.
134
169
 
135
170
  Dynamic routes are used to process responses and perform intermediary steps by the middleware, e.g. request phase,
136
- token verification. While you can define and use same routes within your Rails app, it is highly recommended to modify
137
- your `routes.rb` to perform a dynamic redirect to a another controller method so this does not cause any conflicts with
171
+ token verification. While you can define and use same routes within your Rails app, it is highly recommended to modify
172
+ your `routes.rb` to perform a dynamic redirect to another controller method so this does not cause any conflicts with
138
173
  the middleware or the authorization flow.
139
174
 
140
175
  In an example below, `auth/:provider/callback` is generalized `redirect_uri` value that is passed in the authorization
@@ -163,11 +198,11 @@ end
163
198
  ```
164
199
 
165
200
  **Please note that you should register `https://your_app.com/auth/<simple_provider>/callback` with your OIDC provider
166
- as a callback redirect url.**
201
+ as a callback redirect URL.**
167
202
 
168
203
  ### Using Access Token Without User Info
169
204
 
170
- In case your app requries only an access token and not the user information, then you can specify an optional
205
+ In case your app requires only an access token and not the user information, then you can specify an optional
171
206
  configuration in the omniauth initializer:
172
207
 
173
208
  ```ruby
@@ -185,38 +220,17 @@ Rails.application.config.middleware.use OmniAuth::Builder do
185
220
  end
186
221
  ```
187
222
 
188
- Then the callback returned once your user authenticates with the OIDC provider will contain only access token parameters:
189
-
190
- ```ruby
191
- # app/controllers/callbacks_controller.rb
192
- class CallbacksController < ApplicationController
193
- def omniauth
194
- # access token parameters received from OIDC provider will be available in `request.env['omniauth.auth']`
195
- omniauth_params = request.env['omniauth.auth']
196
-
197
- # omniauth_params will contain similar data as shown below
198
- # {"provider"=>:simple_provider_access_token_only,
199
- # "credentials"=>
200
- # {"id_token"=> "id token value",
201
- # "token"=> "token value",
202
- # "refresh_token"=>"refresh token value",
203
- # "expires_in"=>300,
204
- # "scope"=>nil
205
- # }
206
- # }
207
- end
208
- end
209
- ```
223
+ When `fetch_user_info` is `false`, user info is extracted from the ID token claims instead of calling the userinfo endpoint. If the userinfo endpoint call fails, the gem also falls back to ID token claims automatically.
210
224
 
211
225
  ### Ending Session
212
226
 
213
227
  The gem provides two configuration options to allow ending a session simultaneously with your client application and the
214
228
  OIDC provider.
215
229
 
216
- To use this feature, you need to provide a `logout_path` in the options and an `end_session_endpoint` in the client
217
- options. Heres a sample setup:
230
+ To use this feature, you need to provide a `logout_path` in the options and an `end_session_endpoint` in the client
231
+ options. Here's a sample setup:
218
232
 
219
- ``` ruby
233
+ ```ruby
220
234
  provider :oidc, {
221
235
  name: :simple_provider,
222
236
  client_options: {
@@ -236,32 +250,76 @@ options. Here’s a sample setup:
236
250
  Using these two configurations, you can ensure that when a user logs out from your application, they are also logged out
237
251
  from the OIDC provider, providing a seamless logout across multiple services.
238
252
 
239
- This works by calling `other_phase` on every controller request in your application. The method checks if the requested
240
- URL matches the defined `logout_path`. If it does (i.e. current user has requested to log out from your application)
241
- `other_phase` performs a redirect to the `end_session_endpoint` to terminate the user's session with the OIDC provider
253
+ This works by calling `other_phase` on every controller request in your application. The method checks if the requested
254
+ URL matches the defined `logout_path`. If it does (i.e. current user has requested to log out from your application)
255
+ `other_phase` performs a redirect to the `end_session_endpoint` to terminate the user's session with the OIDC provider
242
256
  and then it returns back to your application and concludes the request to end the current user's session.
243
257
 
244
258
  For additional details please refer to the [OIDC specification](https://openid.net/specs/openid-connect-session-1_0-17.html#:~:text=%C2%A0TOC-,5.%C2%A0%20RP%2DInitiated%20Logout,-An%20RP%20can).
245
259
 
260
+ ### Logging
261
+
262
+ The gem includes built-in logging with automatic sensitive data sanitization. By default, log level is set to WARN.
263
+
264
+ ```ruby
265
+ # Set log level
266
+ OmniauthOidc::Logging.log_level = Logger::INFO
267
+
268
+ # Use a custom logger
269
+ OmniauthOidc::Logging.logger = Rails.logger
270
+ ```
271
+
272
+ If ActiveSupport::Notifications is available (e.g. in Rails), the gem publishes instrumentation events:
273
+
274
+ - `config.fetch.omniauth_oidc` — OIDC discovery document fetch
275
+ - `token.exchange.omniauth_oidc` — authorization code to token exchange
276
+ - `userinfo.fetch.omniauth_oidc` — userinfo endpoint call
277
+ - `jwks.fetch.omniauth_oidc` — JWKS fetch
278
+ - `request_phase.start.omniauth_oidc` — authorization redirect
279
+ - `callback_phase.start.omniauth_oidc` — callback processing
280
+ - `id_token.verify.omniauth_oidc` — ID token verification
281
+
282
+ ### Error Handling
283
+
284
+ The gem raises specific error classes that you can rescue in your callback handling:
285
+
286
+ | Error Class | When Raised |
287
+ |---|---|
288
+ | `OmniauthOidc::MissingConfigurationError` | Required options (`identifier`, `secret`, `config_endpoint`) are missing |
289
+ | `OmniauthOidc::ConfigurationError` | OIDC discovery endpoint fetch fails |
290
+ | `OmniauthOidc::TokenError` | Token exchange fails |
291
+ | `OmniauthOidc::TokenVerificationError` | JWT format is invalid |
292
+ | `OmniauthOidc::TokenExpiredError` | ID token `exp` claim is in the past |
293
+ | `OmniauthOidc::InvalidAlgorithmError` | JWT algorithm does not match `client_signing_alg` |
294
+ | `OmniauthOidc::InvalidSignatureError` | JWT signature verification fails |
295
+ | `OmniauthOidc::InvalidIssuerError` | ID token `iss` does not match expected issuer |
296
+ | `OmniauthOidc::InvalidAudienceError` | ID token `aud` does not match client identifier |
297
+ | `OmniauthOidc::InvalidNonceError` | ID token `nonce` does not match stored nonce |
298
+ | `OmniauthOidc::JwksFetchError` | JWKS endpoint fetch fails |
299
+
300
+ All error classes inherit from `OmniauthOidc::Error < RuntimeError`.
301
+
246
302
  ### Advanced Configuration
303
+
247
304
  You can customize the OIDC strategy further by adding additional configuration options:
248
305
 
249
306
  | Field | Description | Required | Default Value | Example/Notes |
250
307
  |------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|-------------------------------------|-------------------------------------------------------|
251
308
  | name | Arbitrary string to identify OIDC provider and segregate it from other OIDC providers | no | `"oidc"` | `:simple_provider` |
252
- | issuer | Root url for the OIDC authorization server | no | retrived from config_endpoint | `"https://simpleprovider.com"` |
253
- | fetch_user_info | Fetches user information from user_info_endpoint using the access token. If set to false the omniauth params will include only access token | no | `true` | `fetch_user_info: false` |
254
- | client_auth_method | Authentication method to be used with the OIDC authorization server | no | `:basic` | `"basic"`, `"jwks"` |
255
- | scope | OIDC scopes to be included in the server's response | `[:openid]` is required | all scopes offered by OIDC provider | `[:openid, :profile, :email]` |
309
+ | issuer | Root url for the OIDC authorization server | no | retrieved from config_endpoint | `"https://simpleprovider.com"` |
310
+ | fetch_user_info | Fetches user information from userinfo endpoint using the access token. If false, user info is extracted from ID token claims | no | `true` | `fetch_user_info: false` |
311
+ | client_signing_alg | Expected JWT signing algorithm. If set, tokens signed with a different algorithm are rejected | no | `nil` (any algorithm accepted) | `"RS256"`, `"HS256"` |
312
+ | scope | OIDC scopes to request. Accepts Array or String; always sent as a space-delimited string | `openid` is required | all scopes offered by OIDC provider | `%w[openid profile email]` |
256
313
  | response_type | OAuth2 response type expected from OIDC provider during authorization | no | `"code"` | `"code"` or `"id_token"` |
257
314
  | state | Value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string | no | Random 16 character string | `Proc.new { SecureRandom.hex(32) }` |
258
315
  | require_state | Boolean to indicate if state param should be verified. This is a recommendation by OIDC spec | no | `true` | `true` or `false` |
259
316
  | response_mode | The response mode per [OIDC spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | `nil` | `:query`, `:fragment`, `:form_post` or `:web_message` |
260
317
  | display | Specifies how OIDC authorization server should display the authentication and consent UI pages to the end user | no | `nil` | `:page`, `:popup`, `:touch` or `:wap` |
261
318
  | prompt | Specifies whether the OIDC authorization server prompts the end user for reauthentication and consent | no | `nil` | `:none`, `:login`, `:consent` or `:select_account` |
319
+ | send_nonce | Include a nonce in the authorization request and verify it in the ID token | no | `true` | `true` or `false` |
262
320
  | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint | no | `true` | `true` or `false` |
263
321
  | post_logout_redirect_uri | Logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | `nil` | `"https://your_app.com/logout/callback"` |
264
- | uid_field | Field of the user info response to be used as a unique ID | no | `'sub'` | `"sub"` or `"preferred_username"` |
322
+ | uid_field | Field of the user info response to be used as a unique ID | no | `"sub"` | `"sub"` or `"preferred_username"` |
265
323
  | extra_authorize_params | Hash of extra fixed parameters that will be merged to the authorization request | no | `{}` | `{"tenant" => "common"}` |
266
324
  | allow_authorize_params | List of allowed dynamic parameters that will be merged to the authorization request | no | `[]` | `[:screen_name]` |
267
325
  | pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | `false` | `true` or `false` |
@@ -269,9 +327,13 @@ You can customize the OIDC strategy further by adding additional configuration o
269
327
  | pkce_options | Specify custom implementation of the PKCE code challenge/method | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation |
270
328
  | client_options | Hash of client options detailed below in a separate table | yes | see below | see below |
271
329
  | jwt_secret_base64 | Specify the base64-encoded secret used to sign the JWT token for HMAC with SHA2 (e.g. HS256) signing algorithms | no | `client_options.secret` | `"bXlzZWNyZXQ=\n"` |
272
- | logout_path | Log out is only triggered when the request path ends on this path | no | `'/logout'` | '/sign_out' |
273
- | acr_values | Authentication Class Reference (ACR) values to be passed to the authorize_uri to enforce a specific level, see [RFC9470](https://www.rfc-editor.org/rfc/rfc9470.html) | no | `nil` | `"c1 c2"` å|
274
-
330
+ | client_jwk_signing_key | JWK or JWKS (as JSON string or Hash) for local signature verification without fetching from `jwks_uri` | no | `nil` | `'{"kty":"RSA","n":"...","e":"AQAB"}'` |
331
+ | client_x509_signing_key | X.509 certificate (PEM format) for local signature verification | no | `nil` | PEM string |
332
+ | logout_path | Log out is only triggered when the request path ends on this path | no | `"/logout"` | `"/sign_out"` |
333
+ | jwks_cache_ttl | JWKS cache time-to-live in seconds | no | `3600` (1 hour) | `7200` |
334
+ | hd | Google-specific: hosted domain parameter | no | `nil` | `"example.com"` |
335
+ | max_age | Maximum authentication age in seconds | no | `nil` | `3600` |
336
+ | acr_values | Authentication Class Reference (ACR) values per [RFC9470](https://www.rfc-editor.org/rfc/rfc9470.html) | no | `nil` | `"c1 c2"` |
275
337
 
276
338
  Below are options for the `client_options` hash of the configuration:
277
339
 
@@ -280,14 +342,14 @@ Below are options for the `client_options` hash of the configuration:
280
342
  | identifier | OAuth2 client_id | yes | `nil` |
281
343
  | secret | OAuth2 client secret | yes | `nil` |
282
344
  | config_endpoint | OIDC configuration endpoint | yes | `nil` |
283
- | scheme | http scheme to use | no | https |
284
- | host | host of the authorization server | no | nil |
285
- | port | port for the authorization server | no | 443 |
286
- | authorization_endpoint | authorize endpoint on the authorization server | no | retrived from config_endpoint |
287
- | token_endpoint | token endpoint on the authorization server | no | retrived from config_endpoint |
288
- | userinfo_endpoint | user info endpoint on the authorization server | no | retrived from config_endpoint |
289
- | jwks_uri | jwks_uri on the authorization server | no | retrived from config_endpoint |
290
- | end_session_endpoint | url to call to log the user out at the authorization server | no | `nil` |
345
+ | scheme | HTTP scheme to use | no | `"https"` |
346
+ | host | Host of the authorization server | no | `nil` |
347
+ | port | Port for the authorization server | no | `443` |
348
+ | authorization_endpoint | Authorize endpoint on the authorization server | no | retrieved from config_endpoint |
349
+ | token_endpoint | Token endpoint on the authorization server | no | retrieved from config_endpoint |
350
+ | userinfo_endpoint | User info endpoint on the authorization server | no | retrieved from config_endpoint |
351
+ | jwks_uri | JWKS URI on the authorization server | no | retrieved from config_endpoint |
352
+ | end_session_endpoint | URL to call to log the user out at the authorization server | no | `nil` |
291
353
 
292
354
  ## Contributing
293
355
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module OmniauthOidc
6
+ # Custom OIDC client using Net::HTTP
7
+ class Client
8
+ attr_accessor :identifier, :secret, :authorization_endpoint, :token_endpoint, :userinfo_endpoint,
9
+ :host, :redirect_uri
10
+
11
+ def initialize(options = {})
12
+ @identifier = options[:identifier] || options["identifier"]
13
+ @secret = options[:secret] || options["secret"]
14
+ @authorization_endpoint = options[:authorization_endpoint] || options["authorization_endpoint"]
15
+ @token_endpoint = options[:token_endpoint] || options["token_endpoint"]
16
+ @userinfo_endpoint = options[:userinfo_endpoint] || options["userinfo_endpoint"]
17
+ @redirect_uri = options[:redirect_uri] || options["redirect_uri"]
18
+ end
19
+
20
+ def authorization_uri(params = {})
21
+ uri = URI.parse(authorization_endpoint)
22
+ query_params = {
23
+ client_id: identifier,
24
+ response_type: params[:response_type] || "code",
25
+ scope: params[:scope] || "openid profile email",
26
+ redirect_uri: params[:redirect_uri] || @redirect_uri,
27
+ state: params[:state],
28
+ nonce: params[:nonce]
29
+ }.compact
30
+
31
+ # Add PKCE parameters if provided
32
+ query_params[:code_challenge] = params[:code_challenge] if params[:code_challenge]
33
+ query_params[:code_challenge_method] = params[:code_challenge_method] if params[:code_challenge_method]
34
+
35
+ # Add any additional params
36
+ query_params.merge!(params[:extra_params]) if params[:extra_params]
37
+
38
+ uri.query = URI.encode_www_form(query_params)
39
+ uri.to_s
40
+ end
41
+
42
+ def access_token!(params = {}) # rubocop:disable Metrics/MethodLength
43
+ body_params = {
44
+ grant_type: "authorization_code",
45
+ code: params[:code],
46
+ redirect_uri: params[:redirect_uri] || @redirect_uri,
47
+ client_id: identifier,
48
+ client_secret: secret
49
+ }
50
+
51
+ # Add PKCE verifier if provided
52
+ body_params[:code_verifier] = params[:code_verifier] if params[:code_verifier]
53
+
54
+ OmniauthOidc::Logging.instrument("token.exchange", code: "[FILTERED]") do
55
+ response = HttpClient.post(
56
+ token_endpoint,
57
+ body: URI.encode_www_form(body_params),
58
+ headers: {
59
+ "Content-Type" => "application/x-www-form-urlencoded",
60
+ "Accept" => "application/json"
61
+ }
62
+ )
63
+
64
+ ResponseObjects::AccessToken.new(response)
65
+ end
66
+ rescue HttpClient::HttpError => e
67
+ raise OmniauthOidc::TokenError, "Token exchange failed: #{e.message}"
68
+ end
69
+
70
+ def userinfo!(access_token)
71
+ OmniauthOidc::Logging.instrument("userinfo.fetch", endpoint: userinfo_endpoint) do
72
+ response = HttpClient.get(
73
+ userinfo_endpoint,
74
+ headers: {
75
+ "Authorization" => "Bearer #{access_token}",
76
+ "Accept" => "application/json"
77
+ }
78
+ )
79
+
80
+ ResponseObjects::UserInfo.new(response)
81
+ end
82
+ rescue HttpClient::HttpError => e
83
+ raise OmniauthOidc::TokenError, "Failed to fetch user info: #{e.message}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniauthOidc
4
+ # Fetches and parses OpenID Connect configuration from .well-known endpoint
5
+ class ConfigFetcher
6
+ # Config object with dynamic attribute access for OIDC discovery fields
7
+ class Config
8
+ def initialize(attributes = {})
9
+ @attributes = attributes
10
+ end
11
+
12
+ def [](key)
13
+ @attributes[key.to_sym] || @attributes[key.to_s]
14
+ end
15
+
16
+ def []=(key, value)
17
+ @attributes[key.to_sym] = value
18
+ @attributes[key.to_s] = value
19
+ end
20
+
21
+ def respond_to_missing?(method_name, include_private = false)
22
+ setter = method_name.to_s.end_with?("=")
23
+ key = setter ? method_name.to_s.chomp("=") : method_name.to_s
24
+ setter || @attributes.key?(key.to_sym) || @attributes.key?(key) || super
25
+ end
26
+
27
+ private
28
+
29
+ def method_missing(method_name, *args)
30
+ name = method_name.to_s
31
+
32
+ if name.end_with?("=")
33
+ key = name.chomp("=")
34
+ @attributes[key.to_sym] = args.first
35
+ @attributes[key] = args.first
36
+ elsif @attributes.key?(name.to_sym)
37
+ @attributes[name.to_sym]
38
+ elsif @attributes.key?(name)
39
+ @attributes[name]
40
+ else
41
+ super
42
+ end
43
+ end
44
+ end
45
+
46
+ class << self
47
+ def fetch(endpoint_url, max_retries: 3)
48
+ retries = 0
49
+ begin
50
+ response = OmniauthOidc::HttpClient.get(endpoint_url)
51
+ symbolized_config = deep_symbolize_keys(response)
52
+ Config.new(symbolized_config)
53
+ rescue OmniauthOidc::HttpClient::HttpError => e
54
+ retries += 1
55
+ retry if retries < max_retries
56
+ raise OmniauthOidc::ConfigurationError, "Failed to fetch OIDC configuration: #{e.message}"
57
+ rescue StandardError => e
58
+ raise OmniauthOidc::ConfigurationError, "Failed to fetch OIDC configuration: #{e.message}"
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Recursively converts keys of a hash to symbols while retaining the original string keys
65
+ def deep_symbolize_keys(hash)
66
+ result = {}
67
+ hash.each do |key, value|
68
+ sym_key = key.to_sym
69
+ result[sym_key] = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
70
+ result[key] = result[sym_key] # Add the string key as well
71
+ end
72
+ result
73
+ end
74
+ end
75
+ end
76
+ end