Class: Clavis::Providers::Base

Inherits:
Object
  • Object
show all
Includes:
TokenExchangeHandler
Defined in:
lib/clavis/providers/base.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TokenExchangeHandler

#build_token_exchange_params, #handle_connection_error, #handle_error_response, #handle_faraday_error, #handle_parser_error, #handle_standard_error, #make_token_request, #parse_response, #skip_error_for_test?, #test_token_response, #validate_and_clean_code

Constructor Details

#initialize(config = {}) ⇒ Base

Returns a new instance of Base.



29
30
31
32
33
34
35
# File 'lib/clavis/providers/base.rb', line 29

def initialize(config = {})
  @config = config
  set_provider_name
  load_credentials
  setup_endpoints(config)
  validate_configuration!
end

Instance Attribute Details

#authorize_endpoint_urlObject (readonly)

Returns the value of attribute authorize_endpoint_url.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def authorize_endpoint_url
  @authorize_endpoint_url
end

#client_idObject (readonly)

Returns the value of attribute client_id.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def client_id
  @client_id
end

#client_secretObject (readonly)

Returns the value of attribute client_secret.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def client_secret
  @client_secret
end

#provider_nameObject (readonly)

Get the provider name (e.g., :google, :github)



38
39
40
# File 'lib/clavis/providers/base.rb', line 38

def provider_name
  @provider_name
end

#redirect_uriObject (readonly)

Returns the value of attribute redirect_uri.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def redirect_uri
  @redirect_uri
end

#scopeObject (readonly)

Returns the value of attribute scope.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def scope
  @scope
end

#token_endpoint_urlObject (readonly)

Returns the value of attribute token_endpoint_url.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def token_endpoint_url
  @token_endpoint_url
end

#userinfo_endpoint_urlObject (readonly)

Returns the value of attribute userinfo_endpoint_url.



26
27
28
# File 'lib/clavis/providers/base.rb', line 26

def userinfo_endpoint_url
  @userinfo_endpoint_url
end

Instance Method Details

#authorization_endpointObject

Abstract methods that should be implemented by subclasses

Raises:

  • (NotImplementedError)


41
42
43
# File 'lib/clavis/providers/base.rb', line 41

def authorization_endpoint
  raise NotImplementedError, "Subclasses must implement #authorization_endpoint"
end

#authorize_url(state:, nonce:, scope: nil) ⇒ Object



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
191
192
193
# File 'lib/clavis/providers/base.rb', line 163

def authorize_url(state:, nonce:, scope: nil)
  # Validate state and nonce
  raise Clavis::InvalidState unless Clavis::Security::InputValidator.valid_state?(state)
  raise Clavis::InvalidNonce unless Clavis::Security::InputValidator.valid_state?(nonce)

  # Build authorization URL
  params = {
    response_type: "code",
    client_id: client_id,
    redirect_uri: Clavis::Security::HttpsEnforcer.enforce_https(redirect_uri),
    scope: scope || default_scopes,
    state: state
  }

  # Add nonce for OpenID providers
  params[:nonce] = nonce if openid_provider?

  # Add provider-specific params
  params.merge!(additional_authorize_params)

  Clavis::Logging.log_authorization_request(provider_name,
                                            Clavis::Security::ParameterFilter.filter_params(params))

  uri = URI.parse(authorization_endpoint)
  uri.query = URI.encode_www_form(params)

  # Enforce HTTPS for authorization URLs (if configured)
  uri.scheme = "https" if Clavis.configuration.enforce_https && uri.scheme == "http"

  uri.to_s
end

#default_scopesObject



53
54
55
# File 'lib/clavis/providers/base.rb', line 53

def default_scopes
  @scope || Clavis.configuration.default_scopes || "email"
end

#get_user_info(access_token) ⇒ Object



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
# File 'lib/clavis/providers/base.rb', line 125

def (access_token)
  return {} unless userinfo_endpoint

  # Validate the access token - temporarily bypass validation for debugging
  # raise Clavis::InvalidToken unless Clavis::Security::InputValidator.valid_token?(access_token)

  response = http_client.get(userinfo_endpoint) do |req|
    req.headers["Authorization"] = "Bearer #{access_token}"
  end

  if response.status != 200
    Clavis::Logging.log_userinfo_request(provider_name, false)
    handle_userinfo_error_response(response)
  end

  Clavis::Logging.log_userinfo_request(provider_name, true)

  # Parse and validate the response
   = if response.body.is_a?(Hash)
                response.body
              else
                begin
                  parsed = JSON.parse(response.body.to_s, symbolize_names: true)
                  parsed
                rescue JSON::ParserError
                  {}
                end
              end

  # TEMPORARY: Skip validation to debug further
  # unless Clavis::Security::InputValidator.valid_userinfo_response?(user_info)
  #   raise Clavis::InvalidToken, "Invalid user info format"
  # end

  # Sanitize the user info to prevent XSS
  Clavis::Security::InputValidator.sanitize_hash()
end

#openid_provider?Boolean

Returns:

  • (Boolean)


57
58
59
# File 'lib/clavis/providers/base.rb', line 57

def openid_provider?
  false
end

#process_callback(code, user_data = nil) ⇒ Object



195
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
240
241
242
243
244
245
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/clavis/providers/base.rb', line 195

def process_callback(code, user_data = nil)
  Clavis::Logging.debug("#{provider_name}#process_callback - Starting with code: #{code.inspect}")

  # Normalize the code by removing quote characters and trimming whitespace
  clean_code = code.gsub(/^"|"$/, "").strip
  Clavis::Logging.debug("#{provider_name}#process_callback - Cleaned code: #{clean_code}")

  Clavis::Logging.debug("#{provider_name}#process_callback - Calling token_exchange")
  token_data = token_exchange(code: clean_code)
  Clavis::Logging.debug("#{provider_name}#process_callback - Token data received: #{token_data.inspect}")

  # If we have a token, try to get user info
   = {}
  if token_data[:access_token]
    begin
      # Debug log excluding sensitive information
      token_data.except(:access_token, :refresh_token, :id_token)
      Clavis::Logging.debug(
        "#{provider_name}#process_callback - Calling get_user_info with " \
        "access_token: #{token_data[:access_token].inspect}"
      )
       = (token_data[:access_token])
      Clavis::Logging.debug("#{provider_name}#process_callback - User info received: #{user_info.inspect}")
    rescue Clavis::UnsupportedOperation => e
      Clavis::Logging.debug("#{provider_name}#process_callback - UnsupportedOperation: #{e.message}")
       = {}
    rescue StandardError => e
      # Debug log error from user info
      Clavis::Logging.debug(
        "#{provider_name}#process_callback - Error getting user info: " \
        "#{e.class.name}: #{e.message}"
      )
      Clavis::Logging.debug("#{provider_name}#process_callback - Error backtrace: #{e.backtrace.join("\n")}")
      raise
    end
  else
    Clavis::Logging.debug("#{provider_name}#process_callback - No access_token available, skipping user_info")
  end

  # Determine a unique identifier (UID) for the user
  # Prefer ID token claims as the most reliable source
  uid = if token_data[:id_token_claims] && token_data[:id_token_claims][:sub]
          Clavis::Logging.debug("#{provider_name}#process_callback - Using sub from id_token_claims as UID")
          token_data[:id_token_claims][:sub]
        elsif [:sub]
          Clavis::Logging.debug("#{provider_name}#process_callback - Using sub from user_info as UID")
          [:sub]
        elsif [:id]
          Clavis::Logging.debug("#{provider_name}#process_callback - Using id from user_info as UID")
          [:id]
        else
          # Generate a hash of some token data for consistent ids
          Clavis::Logging.debug("#{provider_name}#process_callback - Generating fallback UID")
          data_for_hash = "#{provider_name}:#{token_data[:access_token] || ""}:#{user_info[:email] || ""}"
          Digest::SHA1.hexdigest(data_for_hash)[0...20] # Use first 20 characters for a longer UID
        end

  Clavis::Logging.debug("#{provider_name}#process_callback - UID determined: #{uid}")

  # Merge data from ID token if available
  id_token_claims = {}
  if token_data[:id_token]
    begin
      Clavis::Logging.debug("#{provider_name}#process_callback - Decoding ID token")
      id_token_claims = decode_id_token(token_data[:id_token])
      Clavis::Logging.debug("#{provider_name}#process_callback - ID token claims: #{id_token_claims.inspect}")
    rescue StandardError => e
      Clavis::Logging.debug("#{provider_name}#process_callback - Error decoding ID token: #{e.message}")
      # Don't fail if we can't decode the ID token
    end
  end

  # Debug log ID token claims
  Clavis::Logging.debug(
    "#{provider_name}#process_id_token_if_present - ID token claims: " \
    "#{id_token_claims.inspect}"
  )

  # Do any provider-specific user data processing
  processed_user_data = process_user_data(user_data) if user_data

  # Build the standardized auth hash
  result = {
    provider: provider_name,
    uid: uid,
    info: .merge(processed_user_data || {}),
    credentials: {
      token: token_data[:access_token],
      refresh_token: token_data[:refresh_token],
      expires_at: token_data[:expires_at],
      expires: !token_data[:expires_at].nil?
    }
  }

  # Add ID token if available
  result[:id_token] = token_data[:id_token] if token_data[:id_token]
  if token_data[:id_token_claims] || !id_token_claims.empty?
    result[:id_token_claims] =
      token_data[:id_token_claims] || id_token_claims
  end

  Clavis::Logging.debug("#{provider_name}#process_callback - Returning auth hash: #{result.inspect}")
  result
end

#refresh_token(refresh_token) ⇒ Object



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/clavis/providers/base.rb', line 61

def refresh_token(refresh_token)
  # Validate inputs
  raise Clavis::InvalidToken unless Clavis::Security::InputValidator.valid_token?(refresh_token)

  params = {
    grant_type: "refresh_token",
    refresh_token: refresh_token,
    client_id: client_id,
    client_secret: client_secret
  }

  response = http_client.post(token_endpoint, params)

  if response.status != 200
    Clavis::Logging.log_token_refresh(provider_name, false)
    handle_token_error_response(response)
  end

  Clavis::Logging.log_token_refresh(provider_name, true)

  # Parse and validate the token response
  token_data = parse_token_response(response)

  unless Clavis::Security::InputValidator.valid_token_response?(token_data)
    raise Clavis::InvalidToken, "Invalid token response format"
  end

  # Sanitize the token data to prevent XSS
  Clavis::Security::InputValidator.sanitize_hash(token_data)
end

#token_endpointObject

Raises:

  • (NotImplementedError)


45
46
47
# File 'lib/clavis/providers/base.rb', line 45

def token_endpoint
  raise NotImplementedError, "Subclasses must implement #token_endpoint"
end

#token_exchange(options = {}) ⇒ Object



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
# File 'lib/clavis/providers/base.rb', line 92

def token_exchange(options = {})
  code = options[:code]
  redirect_uri = options[:redirect_uri] || @redirect_uri

  Clavis::Logging.debug("#{provider_name}#token_exchange - Starting with options: #{options.inspect}")

  # Set up the token exchange parameters
  params = build_token_exchange_params(code, redirect_uri)

  # Make the token exchange request
  begin
    response = make_token_request(params)
    token_response = parse_response(response)

    # Check for error
    handle_error_response(token_response, response.status)

    # Log the token exchange
    Clavis::Logging.log_token_exchange(provider_name, true)

    # Return the token data
    process_id_token_if_present(token_response)
  rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
    handle_connection_error(e)
  rescue Faraday::Error => e
    handle_faraday_error(e)
  rescue JSON::ParserError => e
    handle_parser_error(e)
  rescue StandardError => e
    handle_standard_error(e)
  end
end

#userinfo_endpointObject

Raises:

  • (NotImplementedError)


49
50
51
# File 'lib/clavis/providers/base.rb', line 49

def userinfo_endpoint
  raise NotImplementedError, "Subclasses must implement #userinfo_endpoint"
end