Class: Legate::Auth::Schemes::OAuth2
- Inherits:
-
Legate::Auth::Scheme
- Object
- Legate::Auth::Scheme
- Legate::Auth::Schemes::OAuth2
- Defined in:
- lib/legate/auth/schemes/oauth2.rb
Overview
Implements OAuth 2.0 authentication Supports authorization code flow, client credentials flow, and token refresh
Direct Known Subclasses
Instance Attribute Summary collapse
-
#additional_params ⇒ Hash?
readonly
Additional parameters for authorization requests.
-
#authorization_url ⇒ String
readonly
The URL for the authorization endpoint.
-
#revocation_url ⇒ String?
readonly
The URL for the revocation endpoint.
-
#scopes ⇒ Array<String>
readonly
The requested scopes.
-
#token_url ⇒ String
readonly
The URL for the token endpoint.
-
#use_pkce ⇒ Boolean
readonly
Whether to use PKCE.
Instance Method Summary collapse
-
#apply_to_request(request, credential) ⇒ Hash
Applies the OAuth token to a request.
-
#build_authorization_uri(config, redirect_uri = nil, state = nil) ⇒ Hash
Build the authorization URI for the OAuth2 flow.
-
#client_credentials_token(credential) ⇒ Legate::Auth::ExchangedCredential
Exchange client credentials for an access token (client credentials flow).
-
#exchange_token(config, credential) ⇒ Legate::Auth::ExchangedCredential
Exchanges an authorization code for tokens.
-
#initialize(*args, authorization_url: nil, token_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, **kwargs) ⇒ OAuth2
constructor
Initialize a new OAuth2 scheme.
-
#password_token(credential, username, password) ⇒ Legate::Auth::ExchangedCredential
Password flow for getting an access token (resource owner password credentials flow).
-
#refresh_token(exchanged_credential, credential) ⇒ Legate::Auth::ExchangedCredential
Refreshes an access token using a refresh token.
-
#scheme_type ⇒ Symbol
The scheme type.
-
#validate! ⇒ Object
Validates the scheme configuration.
Methods inherited from Legate::Auth::Scheme
#authentication_error?, #revoke_token, #supports_refresh?, #to_h, #to_s
Constructor Details
#initialize(*args, authorization_url: nil, token_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, **kwargs) ⇒ OAuth2
Initialize a new OAuth2 scheme
43 44 45 46 47 48 49 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 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 43 def initialize(*args, authorization_url: nil, token_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, **kwargs) # Handle positional hash parameter (for backward compatibility with child classes like OpenIDConnect) if args.length == 1 && args[0].is_a?(Hash) config = args[0] ||= config[:authorization_url] token_url ||= config[:token_url] scopes ||= config[:scopes] || config[:scope] use_pkce = if config.key?(:use_pkce) config[:use_pkce] else use_pkce.nil? || use_pkce end additional_params ||= config[:additional_params] revocation_url ||= config[:revocation_url] # Extract additional config values kwargs[:client_id] ||= config[:client_id] kwargs[:client_secret] ||= config[:client_secret] kwargs[:redirect_uri] ||= config[:redirect_uri] # Add any remaining config keys to kwargs config.each do |k, v| unless %i[authorization_url token_url scopes scope use_pkce additional_params revocation_url client_id client_secret redirect_uri].include?(k) kwargs[k] ||= v end end end @authorization_url = @token_url = token_url @scopes = parse_scopes(scopes) # Explicitly handle boolean type for use_pkce - preserve false values @use_pkce = use_pkce @additional_params = additional_params @revocation_url = revocation_url # Handle additional parameters which might be passed by derived classes @client_id = kwargs[:client_id] @client_secret = kwargs[:client_secret] @redirect_uri = kwargs[:redirect_uri] # Remaining options @options = kwargs.reject { |k, _| %i[client_id client_secret redirect_uri].include?(k) } # Always validate when this is the base class, not a derived class validate! if self.class == Legate::Auth::Schemes::OAuth2 end |
Instance Attribute Details
#additional_params ⇒ Hash? (readonly)
Returns Additional parameters for authorization requests.
31 32 33 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 31 def additional_params @additional_params end |
#authorization_url ⇒ String (readonly)
Returns The URL for the authorization endpoint.
19 20 21 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 19 def @authorization_url end |
#revocation_url ⇒ String? (readonly)
Returns The URL for the revocation endpoint.
34 35 36 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 34 def revocation_url @revocation_url end |
#scopes ⇒ Array<String> (readonly)
Returns The requested scopes.
25 26 27 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 25 def scopes @scopes end |
#token_url ⇒ String (readonly)
Returns The URL for the token endpoint.
22 23 24 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 22 def token_url @token_url end |
#use_pkce ⇒ Boolean (readonly)
Returns Whether to use PKCE.
28 29 30 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 28 def use_pkce @use_pkce end |
Instance Method Details
#apply_to_request(request, credential) ⇒ Hash
Applies the OAuth token to a request
178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 178 def apply_to_request(request, credential) raise Legate::Auth::CredentialError, 'Expected an exchanged credential' unless credential.is_a?(Legate::Auth::ExchangedCredential) access_token = credential[:access_token] raise Legate::Auth::CredentialError, 'Access token is missing from credential' unless access_token # Apply the access token to the Authorization header validate_header_value!(access_token, 'OAuth2 access token') request[:headers] ||= {} request[:headers]['Authorization'] = "Bearer #{access_token}" request end |
#build_authorization_uri(config, redirect_uri = nil, state = nil) ⇒ Hash
Build the authorization URI for the OAuth2 flow
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 119 def (config, redirect_uri = nil, state = nil) # Get credentials from the config credential = config.credential # Generate state for CSRF protection if not provided state ||= SecureRandom.hex(16) # Build the authorization URL with parameters client_id = credential[:client_id, resolve_env: true] # Create the basic parameters params = { 'client_id' => client_id, 'response_type' => 'code', 'redirect_uri' => redirect_uri, 'state' => state } # Add scopes if present params['scope'] = @scopes.join(' ') if @scopes && !@scopes.empty? # Result hash that will be returned result = { uri: nil, # Will be set below state: state } # Add PKCE if enabled - directly check the instance variable # Only add PKCE if @use_pkce is not false (nil would default to true) if @use_pkce != false code_verifier = SecureRandom.alphanumeric(64) code_challenge = generate_code_challenge(code_verifier) params['code_challenge'] = code_challenge params['code_challenge_method'] = 'S256' result[:pkce] = { code_verifier: code_verifier } end # Add any additional parameters params.merge!(@additional_params) if @additional_params # Remove nil values params.compact! # Build the query string query = URI.encode_www_form(params) # Join with the authorization URL result[:uri] = "#{@authorization_url}?#{query}" result end |
#client_credentials_token(credential) ⇒ Legate::Auth::ExchangedCredential
Exchange client credentials for an access token (client credentials flow)
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 285 def client_credentials_token(credential) # Create an OAuth2 client oauth_client = create_oauth_client(credential) # Request a token using the client credentials flow auth_params = {} auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty? token = oauth_client.client_credentials.get_token(auth_params) # Create an exchanged credential from the token response Legate::Auth::ExchangedCredential.new( auth_type: scheme_type, access_token: token.token, token_type: token.params['token_type'], expires_at: token.expires_at ? Time.at(token.expires_at) : nil, expires_in: token.expires_in, scope: token.params['scope'] ) rescue ::OAuth2::Error => e raise Legate::Auth::TokenExchangeError, "OAuth2 client credentials exchange failed: #{e.}" rescue StandardError => e raise Legate::Auth::TokenExchangeError, "Client credentials exchange failed: #{e.}" end |
#exchange_token(config, credential) ⇒ Legate::Auth::ExchangedCredential
Exchanges an authorization code for tokens
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 196 def exchange_token(config, credential) raise Legate::Auth::TokenExchangeError, 'Response URI is required for token exchange' unless config.response_uri # Extract the code from the response URI uri = URI.parse(config.response_uri) params = CGI.parse(uri.query || '') code = params['code']&.first raise Legate::Auth::TokenExchangeError, 'Authorization code not found in response URI' unless code # Verify the state parameter to prevent CSRF attacks raise Legate::Auth::TokenExchangeError, 'State parameter mismatch' if config.state && params['state']&.first != config.state begin # Create an OAuth2 client oauth_client = create_oauth_client(credential) # Exchange the code for tokens auth_params = { redirect_uri: config.redirect_uri, code: code } # Add PKCE code_verifier if available auth_params[:code_verifier] = config.pkce[:code_verifier] if config.pkce && config.pkce[:code_verifier] token = oauth_client.auth_code.get_token(code, auth_params) # Create an exchanged credential from the token response Legate::Auth::ExchangedCredential.new( auth_type: scheme_type, access_token: token.token, refresh_token: token.refresh_token, token_type: token.params['token_type'], expires_at: token.expires_at ? Time.at(token.expires_at) : nil, expires_in: token.expires_in, scope: token.params['scope'] ) rescue ::OAuth2::Error => e raise Legate::Auth::TokenExchangeError, "OAuth2 token exchange failed: #{e.}" rescue StandardError => e raise Legate::Auth::TokenExchangeError, "Token exchange failed: #{e.}" end end |
#password_token(credential, username, password) ⇒ Legate::Auth::ExchangedCredential
Password flow for getting an access token (resource owner password credentials flow)
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 316 def password_token(credential, username, password) # Create an OAuth2 client oauth_client = create_oauth_client(credential) # Request a token using the password flow auth_params = {} auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty? token = oauth_client.password.get_token(username, password, auth_params) # Create an exchanged credential from the token response Legate::Auth::ExchangedCredential.new( auth_type: scheme_type, access_token: token.token, refresh_token: token.refresh_token, token_type: token.params['token_type'], expires_at: token.expires_at ? Time.at(token.expires_at) : nil, expires_in: token.expires_in, scope: token.params['scope'] ) rescue ::OAuth2::Error => e raise Legate::Auth::TokenExchangeError, "OAuth2 password flow failed: #{e.}" rescue StandardError => e raise Legate::Auth::TokenExchangeError, "Password flow failed: #{e.}" end |
#refresh_token(exchanged_credential, credential) ⇒ Legate::Auth::ExchangedCredential
Refreshes an access token using a refresh token
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 246 def refresh_token(exchanged_credential, credential) refresh_token = exchanged_credential[:refresh_token] raise Legate::Auth::TokenRefreshError, 'Refresh token is missing from credential' unless refresh_token && !refresh_token.empty? begin # Create an OAuth2 client oauth_client = create_oauth_client(credential) # Create a token object with the refresh token token = ::OAuth2::AccessToken.from_hash(oauth_client, { refresh_token: refresh_token, expires_at: exchanged_credential[:expires_at]&.to_i }) # Refresh the token refreshed_token = token.refresh! # Create a new exchanged credential with the refreshed token Legate::Auth::ExchangedCredential.new( auth_type: scheme_type, access_token: refreshed_token.token, refresh_token: refreshed_token.refresh_token || refresh_token, token_type: refreshed_token.params['token_type'], expires_at: refreshed_token.expires_at ? Time.at(refreshed_token.expires_at) : nil, expires_in: refreshed_token.expires_in, scope: refreshed_token.params['scope'] ) rescue ::OAuth2::Error => e raise Legate::Auth::TokenRefreshError, "OAuth2 token refresh failed: #{e.}" rescue StandardError => e raise Legate::Auth::TokenRefreshError, "Token refresh failed: #{e.}" end end |
#scheme_type ⇒ Symbol
Returns The scheme type.
93 94 95 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 93 def scheme_type :oauth2 end |
#validate! ⇒ Object
Validates the scheme configuration
99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/legate/auth/schemes/oauth2.rb', line 99 def validate! # Check if we're in test environment and whether to force validation in_test = ENV['RSPEC_ENV'] == 'test' force_validate = ENV['FORCE_VALIDATE'] == 'true' # Only skip validation in test environment if FORCE_VALIDATE is not true return if in_test && !force_validate raise Legate::Auth::SchemeValidationError, 'Authorization URL is required' if @authorization_url.nil? || @authorization_url.to_s.strip.empty? return unless @token_url.nil? || @token_url.to_s.strip.empty? raise Legate::Auth::SchemeValidationError, 'Token URL is required' end |