omniauth_oidc 0.2.7 → 1.0.1

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.
@@ -5,16 +5,19 @@ require "timeout"
5
5
  require "net/http"
6
6
  require "open-uri"
7
7
  require "omniauth"
8
- require "openid_connect"
9
- require "openid_config_parser"
10
8
  require "forwardable"
11
- require "httparty"
9
+ require "jwt"
10
+ require "ostruct"
11
+ require "openssl"
12
12
 
13
- Dir[File.join(File.dirname(__FILE__), "oidc", "*.rb")].sort.each { |file| require_relative file }
13
+ # Explicit requires instead of Dir glob for clarity and load order control
14
+ require_relative "oidc/request"
15
+ require_relative "oidc/callback"
16
+ require_relative "oidc/verify"
14
17
 
15
18
  module OmniAuth
16
19
  module Strategies
17
- # OIDC strategy for omniauth
20
+ # OIDC strategy for OmniAuth
18
21
  class Oidc
19
22
  include OmniAuth::Strategy
20
23
  include Request
@@ -28,6 +31,8 @@ module OmniAuth
28
31
  "code" => { exception_class: OmniauthOidc::MissingCodeError, key: :missing_code }.freeze
29
32
  }.freeze
30
33
 
34
+ REQUIRED_OPTIONS = %i[identifier secret config_endpoint].freeze
35
+
31
36
  def_delegator :request, :params
32
37
 
33
38
  option :name, :oidc # to separate each oidc provider available in the app
@@ -79,6 +84,9 @@ module OmniAuth
79
84
 
80
85
  option :logout_path, "/logout"
81
86
 
87
+ # JWKS cache configuration
88
+ option :jwks_cache_ttl, 3600 # 1 hour default
89
+
82
90
  def uid
83
91
  user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
84
92
  end
@@ -104,27 +112,44 @@ module OmniAuth
104
112
 
105
113
  credentials do
106
114
  {
107
- id_token: access_token.id_token,
108
- token: access_token.access_token,
109
- refresh_token: access_token.refresh_token,
110
- expires_in: access_token.expires_in,
111
- scope: access_token.scope
115
+ id_token: access_token&.id_token,
116
+ token: access_token&.access_token,
117
+ refresh_token: access_token&.refresh_token,
118
+ expires_in: access_token&.expires_in,
119
+ scope: access_token&.scope
112
120
  }
113
121
  end
114
122
 
115
- # Initialize OpenIDConnect Client with options
123
+ # Initialize our custom OIDC Client with options
116
124
  def client
117
- @client ||= ::OpenIDConnect::Client.new(client_options)
125
+ @client ||= begin
126
+ set_client_endpoints
127
+ OmniauthOidc::Client.new(
128
+ identifier: client_options.identifier,
129
+ secret: client_options.secret,
130
+ authorization_endpoint: client_options.authorization_endpoint,
131
+ token_endpoint: client_options.token_endpoint,
132
+ userinfo_endpoint: client_options.userinfo_endpoint,
133
+ redirect_uri: redirect_uri
134
+ )
135
+ end
136
+ end
137
+
138
+ def set_client_endpoints
139
+ client_options.authorization_endpoint ||= config.authorization_endpoint
140
+ client_options.token_endpoint ||= config.token_endpoint
141
+ client_options.userinfo_endpoint ||= config.userinfo_endpoint
142
+ client_options.jwks_uri ||= config.jwks_uri
143
+ client_options.end_session_endpoint ||= config.end_session_endpoint
118
144
  end
119
145
 
120
- # Config is build from the json response from the OIDC config endpoint
146
+ # Config is built from the json response from the OIDC config endpoint
121
147
  def config
122
- unless client_options.config_endpoint || params["config_endpoint"]
123
- raise Error,
124
- "Configuration endpoint is missing from options"
125
- end
148
+ validate_configuration!
126
149
 
127
- @config ||= OpenidConfigParser.fetch_openid_configuration(client_options.config_endpoint)
150
+ @config ||= OmniauthOidc::Logging.instrument("config.fetch", config_endpoint: client_options.config_endpoint) do
151
+ OmniauthOidc::ConfigFetcher.fetch(client_options.config_endpoint)
152
+ end
128
153
  end
129
154
 
130
155
  # Detects if current request is for the logout url and makes a redirect to end session with OIDC provider
@@ -148,6 +173,18 @@ module OmniAuth
148
173
 
149
174
  private
150
175
 
176
+ def validate_configuration!
177
+ missing = []
178
+ missing << :identifier if client_options.identifier.to_s.empty?
179
+ missing << :secret if client_options.secret.to_s.empty?
180
+ missing << :config_endpoint if client_options.config_endpoint.to_s.empty?
181
+
182
+ return if missing.empty?
183
+
184
+ raise OmniauthOidc::MissingConfigurationError,
185
+ "Missing required configuration: #{missing.join(", ")}"
186
+ end
187
+
151
188
  def issuer
152
189
  @issuer ||= config.issuer
153
190
  end
@@ -158,7 +195,8 @@ module OmniAuth
158
195
 
159
196
  # By default Returns all scopes supported by the OIDC provider
160
197
  def scope
161
- options.scope || config.scopes_supported || [:open_id]
198
+ value = options.scope || config.scopes_supported || [:openid]
199
+ value.is_a?(Array) ? value.join(" ") : value
162
200
  end
163
201
 
164
202
  def authorization_code
@@ -169,12 +207,21 @@ module OmniAuth
169
207
  options.client_options
170
208
  end
171
209
 
210
+ # Session key helpers with provider namespacing
211
+ def session_key(suffix)
212
+ "omniauth.#{name}.#{suffix}"
213
+ end
214
+
172
215
  def stored_state
173
- session.delete("omniauth.state")
216
+ session.delete(session_key("state"))
174
217
  end
175
218
 
176
219
  def new_nonce
177
- session["omniauth.nonce"] = SecureRandom.hex(16)
220
+ session[session_key("nonce")] = SecureRandom.hex(16)
221
+ end
222
+
223
+ def stored_nonce
224
+ session.delete(session_key("nonce"))
178
225
  end
179
226
 
180
227
  def script_name
@@ -210,14 +257,6 @@ module OmniAuth
210
257
  @logout_path_pattern ||= /\A#{Regexp.quote(request.base_url)}#{options.logout_path}/
211
258
  end
212
259
 
213
- # Strips port and host from strings with OIDC endpoints
214
- def resolve_endpoint_from_host(host, endpoint)
215
- start_index = endpoint.index(host) + host.length
216
- endpoint = endpoint[start_index..]
217
- endpoint = "/#{endpoint}" unless endpoint.start_with?("/")
218
- endpoint
219
- end
220
-
221
260
  # Override for the CallbackError class
222
261
  class CallbackError < StandardError
223
262
  attr_accessor :error, :error_reason, :error_uri
@@ -225,8 +264,8 @@ module OmniAuth
225
264
  def initialize(data)
226
265
  super
227
266
  self.error = data[:error]
228
- self.error_reason = data[:reason]
229
- self.error_uri = data[:uri]
267
+ self.error_reason = data[:error_reason] || data[:reason]
268
+ self.error_uri = data[:error_uri] || data[:uri]
230
269
  end
231
270
 
232
271
  def message
data/lib/omniauth_oidc.rb CHANGED
@@ -2,4 +2,11 @@
2
2
 
3
3
  require_relative "omniauth/oidc/version"
4
4
  require_relative "omniauth/oidc/errors"
5
+ require_relative "omniauth/oidc/logging"
6
+ require_relative "omniauth/oidc/jwks_cache"
7
+ require_relative "omniauth/oidc/http_client"
8
+ require_relative "omniauth/oidc/response_objects"
9
+ require_relative "omniauth/oidc/client"
10
+ require_relative "omniauth/oidc/config_fetcher"
11
+ require_relative "omniauth/oidc/jwk_handler"
5
12
  require_relative "omniauth/strategies/oidc"
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
19
  spec.metadata["source_code_uri"] = "https://github.com/msuliq/omniauth_oidc"
20
20
  spec.metadata["changelog_uri"] = "https://github.com/msuliq/omniauth_oidc/blob/main/CHANGELOG.md"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
21
22
 
22
23
  # Specify which files should be added to the gem when it is released.
23
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -31,11 +32,12 @@ Gem::Specification.new do |spec|
31
32
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
33
  spec.require_paths = ["lib"]
33
34
 
34
- # Uncomment to register a new dependency of your gem
35
- spec.add_dependency "httparty"
36
- spec.add_dependency "omniauth"
37
- spec.add_dependency "openid_config_parser"
38
- spec.add_dependency "openid_connect"
35
+ # Runtime dependencies
36
+ spec.add_dependency "jwt", ">= 2.7", "< 4.0"
37
+ spec.add_dependency "omniauth", "~> 2.1"
38
+
39
+ # Ruby 4.0+ compatibility - ostruct no longer in default gems
40
+ # Users on Ruby 4.0+ should add 'gem "ostruct"' to their Gemfile
39
41
 
40
42
  # For more information and examples about making a new gem, check out our
41
43
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -1,4 +1,195 @@
1
1
  module OmniauthOidc
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ # Errors
5
+ class Error < RuntimeError
6
+ end
7
+
8
+ class MissingCodeError < Error
9
+ end
10
+
11
+ class MissingIdTokenError < Error
12
+ end
13
+
14
+ class ConfigurationError < Error
15
+ end
16
+
17
+ class MissingConfigurationError < ConfigurationError
18
+ end
19
+
20
+ class TokenError < Error
21
+ end
22
+
23
+ class TokenVerificationError < TokenError
24
+ end
25
+
26
+ class TokenExpiredError < TokenError
27
+ end
28
+
29
+ class InvalidAlgorithmError < TokenError
30
+ end
31
+
32
+ class InvalidSignatureError < TokenError
33
+ end
34
+
35
+ class InvalidIssuerError < TokenError
36
+ end
37
+
38
+ class InvalidAudienceError < TokenError
39
+ end
40
+
41
+ class InvalidNonceError < TokenError
42
+ end
43
+
44
+ class JwksError < Error
45
+ end
46
+
47
+ class JwksFetchError < JwksError
48
+ end
49
+
50
+ class KeyNotFoundError < JwksError
51
+ end
52
+
53
+ # HTTP Client
54
+ class HttpClient
55
+ class HttpError < StandardError
56
+ end
57
+
58
+ MAX_REDIRECTS: Integer
59
+
60
+ def self.get: (String url, ?headers: Hash[String, String]) -> Hash[String, untyped]
61
+ def self.post: (String url, ?body: String?, ?headers: Hash[String, String]) -> Hash[String, untyped]
62
+ end
63
+
64
+ # OIDC Client
65
+ class Client
66
+ attr_accessor identifier: String?
67
+ attr_accessor secret: String?
68
+ attr_accessor authorization_endpoint: String?
69
+ attr_accessor token_endpoint: String?
70
+ attr_accessor userinfo_endpoint: String?
71
+ attr_accessor host: String?
72
+ attr_accessor redirect_uri: String?
73
+
74
+ def initialize: (?Hash[Symbol | String, untyped] options) -> void
75
+ def authorization_uri: (?Hash[Symbol, untyped] params) -> String
76
+ def access_token!: (?Hash[Symbol, untyped] params) -> ResponseObjects::AccessToken
77
+ def userinfo!: (String access_token) -> ResponseObjects::UserInfo
78
+ end
79
+
80
+ # JWK Handler
81
+ class JwkHandler
82
+ class KeyWithId
83
+ attr_accessor kid: String?
84
+ attr_accessor keypair: OpenSSL::PKey::PKey
85
+
86
+ def initialize: (?kid: String?, ?keypair: OpenSSL::PKey::PKey) -> void
87
+ end
88
+
89
+ def self.parse_jwks: (String | Hash[String, untyped] | nil jwks_data) -> Array[KeyWithId]?
90
+ def self.jwk_to_key: (Hash[String, untyped] jwk_data) -> KeyWithId?
91
+ def self.find_key: (Array[KeyWithId] keys, ?String? kid) -> OpenSSL::PKey::PKey?
92
+ end
93
+
94
+ # JWKS Cache
95
+ class JwksCache
96
+ DEFAULT_TTL: Integer
97
+
98
+ class CacheEntry
99
+ attr_accessor keys: untyped
100
+ attr_accessor fetched_at: Time
101
+
102
+ def initialize: (?keys: untyped, ?fetched_at: Time) -> void
103
+ end
104
+
105
+ def self.instance: () -> JwksCache
106
+ def self.clear!: () -> void
107
+ def self.invalidate: (String jwks_uri) -> void
108
+
109
+ attr_reader ttl: Integer
110
+
111
+ def initialize: (?ttl: Integer) -> void
112
+ def fetch: (String jwks_uri, ?force_refresh: bool) { () -> untyped } -> untyped
113
+ def valid?: (String jwks_uri) -> bool
114
+ def invalidate: (String jwks_uri) -> void
115
+ def clear!: () -> void
116
+ def ttl=: (Integer value) -> void
117
+ end
118
+
119
+ # Config Fetcher
120
+ class ConfigFetcher
121
+ class Config
122
+ def initialize: (?Hash[Symbol | String, untyped] attributes) -> void
123
+ def []: (String | Symbol key) -> untyped
124
+ def []=: (String | Symbol key, untyped value) -> untyped
125
+ def respond_to_missing?: (Symbol method_name, ?bool include_private) -> bool
126
+ end
127
+
128
+ def self.fetch: (String endpoint_url, ?max_retries: Integer) -> Config
129
+ end
130
+
131
+ # Response Objects
132
+ module ResponseObjects
133
+ class IdToken
134
+ attr_reader raw_attributes: Hash[String, untyped]
135
+
136
+ def initialize: (?Hash[String, untyped] attributes) -> void
137
+ def sub: () -> String?
138
+ def iss: () -> String?
139
+ def aud: () -> (String | Array[String])?
140
+ def exp: () -> Integer?
141
+ def iat: () -> Integer?
142
+ def nonce: () -> String?
143
+ end
144
+
145
+ class UserInfo
146
+ attr_reader raw_attributes: Hash[String, untyped]
147
+
148
+ def initialize: (?Hash[String, untyped] attributes) -> void
149
+ def sub: () -> String?
150
+ def name: () -> String?
151
+ def given_name: () -> String?
152
+ def family_name: () -> String?
153
+ def middle_name: () -> String?
154
+ def nickname: () -> String?
155
+ def preferred_username: () -> String?
156
+ def profile: () -> String?
157
+ def picture: () -> String?
158
+ def website: () -> String?
159
+ def email: () -> String?
160
+ def email_verified: () -> bool?
161
+ def gender: () -> String?
162
+ def birthdate: () -> String?
163
+ def zoneinfo: () -> String?
164
+ def locale: () -> String?
165
+ def phone_number: () -> String?
166
+ def phone_number_verified: () -> bool?
167
+ def address: () -> Hash[String, untyped]?
168
+ def updated_at: () -> Integer?
169
+ end
170
+
171
+ class AccessToken
172
+ attr_reader access_token: String?
173
+ attr_reader token_type: String?
174
+ attr_reader expires_in: Integer?
175
+ attr_reader refresh_token: String?
176
+ attr_reader scope: String?
177
+ attr_reader id_token: String?
178
+
179
+ def initialize: (?Hash[String | Symbol, untyped] attributes) -> void
180
+ def to_h: () -> Hash[String, untyped]
181
+ end
182
+ end
183
+
184
+ # Logging
185
+ module Logging
186
+ def self.logger: () -> Logger
187
+ def self.logger=: (Logger logger) -> void
188
+ def self.log_level=: (Integer level) -> void
189
+ def self.instrument: (String event_name, ?Hash[Symbol, untyped] payload) ?{ (Hash[Symbol, untyped]) -> untyped } -> untyped
190
+ def self.debug: (String message, ?Hash[Symbol, untyped] context) -> void
191
+ def self.info: (String message, ?Hash[Symbol, untyped] context) -> void
192
+ def self.warn: (String message, ?Hash[Symbol, untyped] context) -> void
193
+ def self.error: (String message, ?Hash[Symbol, untyped] context) -> void
194
+ end
4
195
  end
metadata CHANGED
@@ -1,71 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth_oidc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Suleyman Musayev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-10 00:00:00.000000000 Z
11
+ date: 2026-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: httparty
14
+ name: jwt
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: omniauth
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
19
+ version: '2.7'
20
+ - - "<"
32
21
  - !ruby/object:Gem::Version
33
- version: '0'
22
+ version: '4.0'
34
23
  type: :runtime
35
24
  prerelease: false
36
25
  version_requirements: !ruby/object:Gem::Requirement
37
26
  requirements:
38
27
  - - ">="
39
28
  - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: openid_config_parser
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
29
+ version: '2.7'
30
+ - - "<"
53
31
  - !ruby/object:Gem::Version
54
- version: '0'
32
+ version: '4.0'
55
33
  - !ruby/object:Gem::Dependency
56
- name: openid_connect
34
+ name: omniauth
57
35
  requirement: !ruby/object:Gem::Requirement
58
36
  requirements:
59
- - - ">="
37
+ - - "~>"
60
38
  - !ruby/object:Gem::Version
61
- version: '0'
39
+ version: '2.1'
62
40
  type: :runtime
63
41
  prerelease: false
64
42
  version_requirements: !ruby/object:Gem::Requirement
65
43
  requirements:
66
- - - ">="
44
+ - - "~>"
67
45
  - !ruby/object:Gem::Version
68
- version: '0'
46
+ version: '2.1'
69
47
  description: |-
70
48
  Omniauth strategy to authenticate and retrieve user data as a client using OpenID Connect (OIDC)
71
49
  suited for multiple OIDC providers.
@@ -81,7 +59,14 @@ files:
81
59
  - LICENSE.txt
82
60
  - README.md
83
61
  - Rakefile
62
+ - lib/omniauth/oidc/client.rb
63
+ - lib/omniauth/oidc/config_fetcher.rb
84
64
  - lib/omniauth/oidc/errors.rb
65
+ - lib/omniauth/oidc/http_client.rb
66
+ - lib/omniauth/oidc/jwk_handler.rb
67
+ - lib/omniauth/oidc/jwks_cache.rb
68
+ - lib/omniauth/oidc/logging.rb
69
+ - lib/omniauth/oidc/response_objects.rb
85
70
  - lib/omniauth/oidc/version.rb
86
71
  - lib/omniauth/strategies/oidc.rb
87
72
  - lib/omniauth/strategies/oidc/callback.rb
@@ -97,6 +82,7 @@ metadata:
97
82
  homepage_uri: https://github.com/msuliq/omniauth_oidc
98
83
  source_code_uri: https://github.com/msuliq/omniauth_oidc
99
84
  changelog_uri: https://github.com/msuliq/omniauth_oidc/blob/main/CHANGELOG.md
85
+ rubygems_mfa_required: 'true'
100
86
  post_install_message:
101
87
  rdoc_options: []
102
88
  require_paths: