Class: Legate::Auth::Schemes::OpenIDConnect
- Inherits:
-
OAuth2
- Object
- Legate::Auth::Scheme
- OAuth2
- Legate::Auth::Schemes::OpenIDConnect
- Defined in:
- lib/legate/auth/schemes/openid_connect.rb
Overview
Implements OpenID Connect authentication Extends OAuth2 with OpenID Connect specific features
Instance Attribute Summary collapse
-
#authorization_url ⇒ Object
readonly
Override to prevent the base URL from being modified with default query parameters Just return the base authorization_url without query parameters.
-
#client_id ⇒ String
readonly
The client ID.
-
#discovery_url ⇒ String?
readonly
The URL for the OpenID Connect discovery document.
-
#issuer ⇒ String?
readonly
The issuer identifier.
-
#jwks_url ⇒ String?
readonly
The URL for the JWK Set.
-
#provider_uri ⇒ String?
readonly
The provider URI.
-
#userinfo_url ⇒ String?
readonly
The userinfo endpoint URL.
Attributes inherited from OAuth2
#additional_params, #revocation_url, #scopes, #token_url, #use_pkce
Instance Method Summary collapse
-
#build_authorization_uri(config, redirect_uri = nil, state = nil) ⇒ Hash
Build the authorization URI for the OpenID Connect flow.
-
#discover_endpoints ⇒ Hash
Discover OpenID Connect endpoints from the discovery URL.
-
#exchange_token(config, credential) ⇒ Legate::Auth::ExchangedCredential
Override exchange_token to set correct auth_type.
-
#get_userinfo(access_token) ⇒ Hash
Retrieve user information using the access token.
-
#initialize(first_arg = nil, authorization_url: nil, token_url: nil, discovery_url: nil, jwks_url: nil, userinfo_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, client_id: nil, client_secret: nil, redirect_uri: nil, **kwargs) ⇒ OpenIDConnect
constructor
Initialize a new OpenID Connect scheme.
-
#scheme_type ⇒ Symbol
The scheme type.
-
#to_h ⇒ Hash
Convert to a hash representation.
-
#validate! ⇒ Object
Validates the scheme configuration.
-
#verify_id_token(id_token, nonce = nil, audience = nil) ⇒ Hash
Verify an ID token using the provider’s JWKS for signature verification.
Methods inherited from OAuth2
#apply_to_request, #client_credentials_token, #password_token, #refresh_token
Methods inherited from Legate::Auth::Scheme
#apply_to_request, #authentication_error?, #refresh_token, #revoke_token, #supports_refresh?, #to_s
Constructor Details
#initialize(first_arg = nil, authorization_url: nil, token_url: nil, discovery_url: nil, jwks_url: nil, userinfo_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, client_id: nil, client_secret: nil, redirect_uri: nil, **kwargs) ⇒ OpenIDConnect
Initialize a new OpenID Connect scheme
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 50 def initialize(first_arg = nil, authorization_url: nil, token_url: nil, discovery_url: nil, jwks_url: nil, userinfo_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, client_id: nil, client_secret: nil, redirect_uri: nil, **kwargs) # Handle direct hash configuration in first_arg or config param config = first_arg if first_arg.is_a?(Hash) if config.is_a?(Hash) # Extract OpenID Connect specific properties from config @discovery_url = config[:discovery_url] || config[:provider_uri] && "#{config[:provider_uri]}/.well-known/openid-configuration" @jwks_url = config[:jwks_url] @userinfo_url = config[:userinfo_url] @client_id = config[:client_id] @client_secret = config[:client_secret] @redirect_uri = config[:redirect_uri] @provider_uri = config[:provider_uri] @issuer = config[:issuer] = config[:authorization_url] || config[:authorization_endpoint] token_url = config[:token_url] || config[:token_endpoint] scopes = config[:scopes] || config[:scope] use_pkce = config.key?(:use_pkce) ? config[:use_pkce] : true additional_params = config[:additional_params] revocation_url = config[:revocation_url] # Move any remaining options to kwargs extra_opts = config.reject { |k, _| %i[discovery_url jwks_url userinfo_url client_id client_secret redirect_uri provider_uri issuer authorization_url authorization_endpoint token_url token_endpoint scopes scope use_pkce additional_params revocation_url].include?(k) } kwargs = kwargs.merge(extra_opts) else # Store OpenID Connect specific properties from parameters @discovery_url = discovery_url @jwks_url = jwks_url @userinfo_url = userinfo_url @client_id = client_id @client_secret = client_secret @redirect_uri = redirect_uri @provider_uri = kwargs[:provider_uri] @issuer = kwargs[:issuer] end # If discovery URL is provided, try to fetch endpoints if @discovery_url && (.nil? || token_url.nil? || @userinfo_url.nil?) endpoints = discover_endpoints ||= endpoints[:authorization_endpoint] token_url ||= endpoints[:token_endpoint] @jwks_url ||= endpoints[:jwks_uri] @userinfo_url ||= endpoints[:userinfo_endpoint] @issuer ||= endpoints[:issuer] end # Parse and add the openid scope if not present oidc_scopes = parse_scopes(scopes) oidc_scopes << 'openid' unless oidc_scopes.include?('openid') # Call the parent constructor with merged settings super( authorization_url: , token_url: token_url, scopes: oidc_scopes, use_pkce: use_pkce, additional_params: additional_params, revocation_url: revocation_url, client_id: @client_id, client_secret: @client_secret, redirect_uri: @redirect_uri, **kwargs ) # Make sure client_id is properly set after parent initialization @client_id = kwargs[:client_id] if @client_id.nil? # Validate required fields if this is a direct instance (not a subclass) validate! if self.class == Legate::Auth::Schemes::OpenIDConnect end |
Instance Attribute Details
#authorization_url ⇒ Object (readonly)
Override to prevent the base URL from being modified with default query parameters Just return the base authorization_url without query parameters
152 153 154 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 152 def @authorization_url end |
#client_id ⇒ String (readonly)
Returns The client ID.
33 34 35 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 33 def client_id @client_id end |
#discovery_url ⇒ String? (readonly)
Returns The URL for the OpenID Connect discovery document.
18 19 20 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 18 def discovery_url @discovery_url end |
#issuer ⇒ String? (readonly)
Returns The issuer identifier.
27 28 29 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 27 def issuer @issuer end |
#jwks_url ⇒ String? (readonly)
Returns The URL for the JWK Set.
21 22 23 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 21 def jwks_url @jwks_url end |
#provider_uri ⇒ String? (readonly)
Returns The provider URI.
30 31 32 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 30 def provider_uri @provider_uri end |
#userinfo_url ⇒ String? (readonly)
Returns The userinfo endpoint URL.
24 25 26 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 24 def userinfo_url @userinfo_url end |
Instance Method Details
#build_authorization_uri(config, redirect_uri = nil, state = nil) ⇒ Hash
Build the authorization URI for the OpenID Connect flow
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 159 def (config, redirect_uri = nil, state = nil) # Generate nonce for OpenID Connect nonce = config.[:nonce] || SecureRandom.hex(16) # Store nonce in config for later verification config.[:nonce] = nonce # Add nonce to parameters additional_params = @additional_params ? @additional_params.dup : {} additional_params['nonce'] = nonce # Ensure 'openid' scope is included oidc_scopes = @scopes.dup oidc_scopes << 'openid' unless oidc_scopes.include?('openid') # Temporarily store modified scopes original_scopes = @scopes @scopes = oidc_scopes # Temporarily modify additional_params original_additional_params = @additional_params @additional_params = additional_params # Call the parent method result = super(config, redirect_uri, state) # Restore original additional_params and scopes @additional_params = original_additional_params @scopes = original_scopes result end |
#discover_endpoints ⇒ Hash
Discover OpenID Connect endpoints from the discovery URL
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 217 def discover_endpoints return {} unless @discovery_url # Skip discovery in test environment to avoid HTTP calls return {} if ENV['RSPEC_ENV'] == 'test' begin validate_auth_url!(@discovery_url, label: 'Discovery URL') uri = URI(@discovery_url) response = Net::HTTP.get_response(uri) unless response.is_a?(Net::HTTPSuccess) Legate.logger.error("Failed to fetch OpenID Connect discovery document: #{response.code} #{response.}") return {} end discovery_data = JSON.parse(response.body) { authorization_endpoint: discovery_data['authorization_endpoint'], token_endpoint: discovery_data['token_endpoint'], jwks_uri: discovery_data['jwks_uri'], userinfo_endpoint: discovery_data['userinfo_endpoint'], issuer: discovery_data['issuer'] } rescue StandardError => e Legate.logger.error("Error discovering OpenID Connect endpoints: #{e.}") {} end end |
#exchange_token(config, credential) ⇒ Legate::Auth::ExchangedCredential
Override exchange_token to set correct auth_type
196 197 198 199 200 201 202 203 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 196 def exchange_token(config, credential) result = super(config, credential) # Modify the auth_type to be :openid_connect if successful result.instance_variable_set(:@auth_type, :openid_connect) if result && result.is_a?(Legate::Auth::ExchangedCredential) result end |
#get_userinfo(access_token) ⇒ Hash
Retrieve user information using the access token
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 252 def get_userinfo(access_token) # Use the configured userinfo endpoint, fall back to issuer-based URL endpoint = @userinfo_url endpoint = "#{@issuer}/userinfo" if endpoint.nil? && @issuer raise Legate::Auth::Errors::AuthenticationError, 'Userinfo endpoint not configured' unless endpoint begin validate_auth_url!(endpoint, label: 'Userinfo URL') response = Faraday.get(endpoint) do |req| req.headers['Authorization'] = "Bearer #{access_token}" end raise Legate::Auth::Errors::AuthenticationError, "Failed to fetch userinfo: #{response.status} #{response.reason_phrase}" unless response.status == 200 JSON.parse(response.body) rescue Faraday::Error => e raise Legate::Auth::Errors::AuthenticationError, "Error fetching userinfo: #{e.}" rescue JSON::ParserError => e raise Legate::Auth::Errors::AuthenticationError, "Invalid userinfo response: #{e.}" rescue StandardError => e raise Legate::Auth::Errors::AuthenticationError, "Unexpected error fetching userinfo: #{e.}" end end |
#scheme_type ⇒ Symbol
Returns The scheme type.
130 131 132 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 130 def scheme_type :openid_connect end |
#to_h ⇒ Hash
Convert to a hash representation
207 208 209 210 211 212 213 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 207 def to_h hash = super hash[:discovery_url] = @discovery_url if @discovery_url hash[:jwks_url] = @jwks_url if @jwks_url hash[:userinfo_url] = @userinfo_url if @userinfo_url hash end |
#validate! ⇒ Object
Validates the scheme configuration
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 136 def validate! # Only skip validation in test environment if FORCE_VALIDATE is not true in_test = ENV['RSPEC_ENV'] == 'test' force_validate = ENV['FORCE_VALIDATE'] == 'true' return if in_test && !force_validate raise Legate::Auth::SchemeValidationError, 'Authorization URL is required' if .nil? || .to_s.strip.empty? return unless token_url.nil? || token_url.to_s.strip.empty? raise Legate::Auth::SchemeValidationError, 'Token URL is required' end |
#verify_id_token(id_token, nonce = nil, audience = nil) ⇒ Hash
Verify an ID token using the provider’s JWKS for signature verification.
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/legate/auth/schemes/openid_connect.rb', line 283 def verify_id_token(id_token, nonce = nil, audience = nil) jwks = fetch_jwks algorithms = %w[RS256 RS384 RS512 ES256 ES384 ES512] decode_opts = { algorithms: algorithms } decode_opts[:iss] = @issuer if @issuer decode_opts[:verify_iss] = true if @issuer decode_opts[:aud] = audience if audience decode_opts[:verify_aud] = true if audience if jwks && !jwks.empty? jwk_set = JWT::JWK::Set.new(jwks) payload, _header = JWT.decode(id_token, nil, true, decode_opts) do |header| jwk_set.find { |key| key[:kid] == header['kid'] }&.public_key end else # No JWKS available — decode without signature verification # but still validate claims. Log a warning since this weakens security. Legate.logger.warn('OpenIDConnect: No JWKS available for signature verification — decoding without signature check') payload, _header = JWT.decode(id_token, nil, false, algorithms: algorithms) end raise Legate::Auth::TokenVerificationError, 'ID token nonce mismatch' if nonce && payload['nonce'] != nonce payload rescue JWT::DecodeError => e raise Legate::Auth::TokenVerificationError, "Failed to verify ID token: #{e.}" rescue StandardError => e raise Legate::Auth::TokenVerificationError, "ID token verification failed: #{e.}" end |