Class: Clavis::Providers::Google

Inherits:
Base
  • Object
show all
Defined in:
lib/clavis/providers/google.rb

Constant Summary collapse

ALLOWED_ISSUERS =
["accounts.google.com", "https://accounts.google.com"].freeze

Instance Attribute Summary

Attributes inherited from Base

#authorize_endpoint_url, #client_id, #client_secret, #provider_name, #redirect_uri, #scope, #token_endpoint_url, #userinfo_endpoint_url

Instance Method Summary collapse

Methods inherited from Base

#process_callback, #refresh_token, #token_exchange

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 = {}) ⇒ Google

Returns a new instance of Google.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/clavis/providers/google.rb', line 12

def initialize(config = {})
  # Validate required fields first
  if config[:client_id].nil? || config[:client_id].empty?
    raise Clavis::MissingConfiguration,
          "client_id for google"
  end
  if config[:client_secret].nil? || config[:client_secret].empty?
    raise Clavis::MissingConfiguration,
          "client_secret for google"
  end
  if config[:redirect_uri].nil? || config[:redirect_uri].empty?
    raise Clavis::MissingConfiguration,
          "redirect_uri for google"
  end

  # Set endpoints
  config[:authorization_endpoint] = "https://accounts.google.com/o/oauth2/v2/auth"
  config[:token_endpoint] = "https://oauth2.googleapis.com/token"
  config[:userinfo_endpoint] = "https://www.googleapis.com/oauth2/v3/userinfo"
  config[:scope] = config[:scope] || "openid email profile"

  # Set configurable options with defaults
  @jwt_leeway = config[:jwt_leeway] || 60
  @token_verification_enabled = config[:verify_tokens] != false
  @hosted_domain = config[:hosted_domain]
  @allowed_hosted_domains = Array(@hosted_domain) if @hosted_domain

  super
end

Instance Method Details

#authorization_endpointObject



42
43
44
# File 'lib/clavis/providers/google.rb', line 42

def authorization_endpoint
  "https://accounts.google.com/o/oauth2/v2/auth"
end

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

Raises:



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

def authorize_url(state:, nonce:, scope: nil, login_hint: nil, prompt: 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: normalize_scopes(scope || default_scopes),
    state: state,
    nonce: nonce,
    access_type: "offline"
  }

  # Add optional parameters if provided
  params[:login_hint] =  if 
  params[:prompt] = prompt || "consent" # Default to consent to ensure refresh token
  params[:hd] = @hosted_domain if @hosted_domain && @hosted_domain != "*"

  Clavis::Logging.log_authorization_request(provider_name, params)

  "#{authorization_endpoint}?#{to_query(params)}"
end

#default_scopesObject



58
59
60
# File 'lib/clavis/providers/google.rb', line 58

def default_scopes
  "openid email profile"
end

#get_user_info(access_token) ⇒ Object

Override to add token verification



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

def (access_token)
  # Extract the token string from the access_token parameter
  token_str = case access_token
              when Hash
                token_val = access_token[:access_token] || access_token["access_token"]
                token_val
              when String
                access_token
              else
                access_token.to_s
              end

  # Validate the access token if token verification is enabled
  if @token_verification_enabled
    verified = verify_token(access_token)
    raise Clavis::InvalidToken, "Access token verification failed" unless verified
  end

  # Get the user info from the Google API
   = super(token_str)

  # Verify the hosted domain if configured
  verify_hosted_domain() if  && !.empty?

   || {}
end

#normalize_scopes(scope_string) ⇒ Object

Enhanced scope handling inspired by OmniAuth



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/clavis/providers/google.rb', line 67

def normalize_scopes(scope_string)
  return default_scopes if scope_string.nil? || scope_string.empty?

  # Handle both space and comma-delimited scopes
  scopes = scope_string.split(/[\s,]+/)

  # Add default base scopes if not explicitly included
  base_scopes = %w[openid email profile]
  base_scopes.each do |base_scope|
    scopes << base_scope unless scopes.include?(base_scope)
  end

  scopes.uniq.join(" ")
end

#openid_provider?Boolean

Returns:



62
63
64
# File 'lib/clavis/providers/google.rb', line 62

def openid_provider?
  true
end

#token_endpointObject



46
47
48
# File 'lib/clavis/providers/google.rb', line 46

def token_endpoint
  "https://oauth2.googleapis.com/token"
end

#tokeninfo_endpointObject



54
55
56
# File 'lib/clavis/providers/google.rb', line 54

def tokeninfo_endpoint
  "https://www.googleapis.com/oauth2/v3/tokeninfo"
end

#userinfo_endpointObject



50
51
52
# File 'lib/clavis/providers/google.rb', line 50

def userinfo_endpoint
  "https://www.googleapis.com/oauth2/v3/userinfo"
end

#verify_hosted_domain(user_info) ⇒ Object

Verify hosted domain if configured



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/clavis/providers/google.rb', line 191

def verify_hosted_domain()
  return true unless @hosted_domain
  return true if @hosted_domain == "*"

  user_hd = [:hd]

  if user_hd.nil? || !@allowed_hosted_domains.include?(user_hd)
    Clavis::Logging.log_hosted_domain_verification(provider_name, false,
                                                   "Expected #{@allowed_hosted_domains}, got #{user_hd}")
    raise Clavis::InvalidHostedDomain, "User is not a member of the allowed hosted domain"
  end

  Clavis::Logging.log_hosted_domain_verification(provider_name, true)
  true
end

#verify_id_token(id_token) ⇒ Object

Verify ID token with more comprehensive checks



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/clavis/providers/google.rb', line 109

def verify_id_token(id_token)
  return {} if id_token.nil? || id_token.empty?

  begin
    # Decode without verification first to get the header and payload
    decoded_segments = ::JWT.decode(id_token, nil, false)
    decoded = decoded_segments.first

    # Now verify claims
    validate_id_token_claims!(decoded)

    decoded
  rescue ::JWT::DecodeError => e
    Clavis::Logging.log_token_verification(provider_name, false, "JWT decode error: #{e.message}")
    raise Clavis::InvalidToken, "Invalid ID token format"
  rescue StandardError => e
    Clavis::Logging.log_token_verification(provider_name, false, "Token verification error: #{e.message}")
    raise Clavis::InvalidToken, "ID token verification failed"
  end
end

#verify_token(access_token) ⇒ Object



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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/clavis/providers/google.rb', line 130

def verify_token(access_token)
  return false unless @token_verification_enabled

  # Extract the token string from the access_token parameter
  token_str = case access_token
              when Hash
                token_val = access_token[:access_token] || access_token["access_token"]
                token_val
              when String
                access_token
              else
                access_token.to_s
              end

  return false if token_str.nil? || token_str.empty?

  begin
    response = http_client.get(tokeninfo_endpoint) do |req|
      req.params[:access_token] = token_str
    end

    # If status is not 200, we can immediately return false without parsing the body
    if response.status != 200
      Clavis::Logging.log_token_verification(provider_name, false, "Token info response: #{response.status}")
      return false
    end

    # Process response body based on what Faraday gives us
    token_info = {}

    # Faraday's response.body could be a Hash (with JSON middleware) or a String
    if response.body.is_a?(Hash)
      # Symbolize keys for consistency
      token_info = response.body.transform_keys(&:to_sym)
    elsif response.body.is_a?(String) && !response.body.empty?
      begin
        token_info = JSON.parse(response.body, symbolize_names: true)
      rescue JSON::ParserError
        Clavis::Logging.log_token_verification(provider_name, false, "Invalid JSON response")
        return false
      end
    else
      return false
    end

    # Verify the audience matches our client_id
    if token_info[:aud] != client_id
      Clavis::Logging.log_token_verification(provider_name, false, "Token audience mismatch")
      return false
    end

    # If we get here, the token is valid
    Clavis::Logging.log_token_verification(provider_name, true)
    true
  rescue StandardError => e
    Clavis::Logging.log_token_verification(provider_name, false, e.message)
    false
  end
end