summaryrefslogtreecommitdiff
path: root/vendor/gems/omniauth-google-oauth2/lib/omniauth/strategies/google_oauth2.rb
blob: 4ce32eb80d1636b65aa5b1e74313feb17992f570 (plain)
1
2
3
4
5
6
7
8
9
10
11
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
41
42
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
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
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
# frozen_string_literal: true

require 'jwt'
require 'oauth2'
require 'omniauth/strategies/oauth2'
require 'uri'

module OmniAuth
  module Strategies
    # Main class for Google OAuth2 strategy.
    class GoogleOauth2 < OmniAuth::Strategies::OAuth2
      ALLOWED_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'].freeze
      BASE_SCOPE_URL = 'https://www.googleapis.com/auth/'
      BASE_SCOPES = %w[profile email openid].freeze
      DEFAULT_SCOPE = 'email,profile'
      USER_INFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
      IMAGE_SIZE_REGEXP = /(s\d+(-c)?)|(w\d+-h\d+(-c)?)|(w\d+(-c)?)|(h\d+(-c)?)|c/

      option :name, 'google_oauth2'
      option :skip_friends, true
      option :skip_image_info, true
      option :skip_jwt, false
      option :jwt_leeway, 60
      option :authorize_options, %i[access_type hd login_hint prompt request_visible_actions scope state redirect_uri include_granted_scopes openid_realm device_id device_name]
      option :authorized_client_ids, []

      option :client_options,
             site: 'https://oauth2.googleapis.com',
             authorize_url: 'https://accounts.google.com/o/oauth2/auth',
             token_url: '/token'

      def authorize_params
        super.tap do |params|
          options[:authorize_options].each do |k|
            params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
          end

          params[:scope] = get_scope(params)
          params[:access_type] = 'offline' if params[:access_type].nil?
          params['openid.realm'] = params.delete(:openid_realm) unless params[:openid_realm].nil?

          session['omniauth.state'] = params[:state] if params[:state]
        end
      end

      uid { raw_info['sub'] }

      info do
        prune!(
          name: raw_info['name'],
          email: verified_email,
          unverified_email: raw_info['email'],
          email_verified: raw_info['email_verified'],
          first_name: raw_info['given_name'],
          last_name: raw_info['family_name'],
          image: image_url,
          urls: {
            google: raw_info['profile']
          }
        )
      end

      credentials do
        # Tokens and expiration will be used from OAuth2 strategy credentials block
        prune!({ 'scope' => token_info(access_token.token)['scope'] })
      end

      extra do
        hash = {}
        hash[:id_token] = access_token['id_token']
        if !options[:skip_jwt] && !access_token['id_token'].nil?
          decoded = ::JWT.decode(access_token['id_token'], nil, false).first

          # We have to manually verify the claims because the third parameter to
          # JWT.decode is false since no verification key is provided.
          ::JWT::Verify.verify_claims(decoded,
                                      verify_iss: true,
                                      iss: ALLOWED_ISSUERS,
                                      verify_aud: true,
                                      aud: options.client_id,
                                      verify_sub: false,
                                      verify_expiration: true,
                                      verify_not_before: true,
                                      verify_iat: false,
                                      verify_jti: false,
                                      leeway: options[:jwt_leeway])

          hash[:id_info] = decoded
        end
        hash[:raw_info] = raw_info unless skip_info?
        prune! hash
      end

      def raw_info
        @raw_info ||= access_token.get(USER_INFO_URL).parsed
      end

      def custom_build_access_token
        access_token = get_access_token(request)

        verify_hd(access_token)
        access_token
      end

      alias build_access_token custom_build_access_token

      private

      def callback_url
        options[:redirect_uri] || (full_host + callback_path)
      end

      def get_access_token(request)
        verifier = request.params['code']
        redirect_uri = request.params['redirect_uri']
        access_token = request.params['access_token']
        if verifier && request.xhr?
          client_get_token(verifier, redirect_uri || 'postmessage')
        elsif verifier
          client_get_token(verifier, redirect_uri || callback_url)
        elsif access_token && verify_token(access_token)
          ::OAuth2::AccessToken.from_hash(client, request.params.dup)
        elsif request.content_type =~ /json/i
          begin
            body = JSON.parse(request.body.read)
            request.body.rewind # rewind request body for downstream middlewares
            verifier = body && body['code']
            access_token = body && body['access_token']
            redirect_uri ||= body && body['redirect_uri']
            if verifier
              client_get_token(verifier, redirect_uri || 'postmessage')
            elsif verify_token(access_token)
              ::OAuth2::AccessToken.from_hash(client, body.dup)
            end
          rescue JSON::ParserError => e
            warn "[omniauth google-oauth2] JSON parse error=#{e}"
          end
        end
      end

      def client_get_token(verifier, redirect_uri)
        client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params)
      end

      def get_token_params
        deep_symbolize(options.auth_token_params || {})
      end

      def get_scope(params)
        raw_scope = params[:scope] || DEFAULT_SCOPE
        scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten
        scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" }
        scope_list.join(' ')
      end

      def verified_email
        raw_info['email_verified'] ? raw_info['email'] : nil
      end

      def get_token_options(redirect_uri = '')
        { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true))
      end

      def prune!(hash)
        hash.delete_if do |_, v|
          prune!(v) if v.is_a?(Hash)
          v.nil? || (v.respond_to?(:empty?) && v.empty?)
        end
      end

      def image_url
        return nil unless raw_info['picture']

        u = URI.parse(raw_info['picture'].gsub('https:https', 'https'))

        path_index = u.path.to_s.index('/photo.jpg')

        if path_index && image_size_opts_passed?
          u.path.insert(path_index, image_params)
          u.path = u.path.gsub('//', '/')

          # Check if the image is already sized!
          split_path = u.path.split('/')
          u.path = u.path.sub("/#{split_path[-3]}", '') if split_path[-3] =~ IMAGE_SIZE_REGEXP
        end

        u.query = strip_unnecessary_query_parameters(u.query)

        u.to_s
      end

      def image_size_opts_passed?
        options[:image_size] || options[:image_aspect_ratio]
      end

      def image_params
        image_params = []
        if options[:image_size].is_a?(Integer)
          image_params << "s#{options[:image_size]}"
        elsif options[:image_size].is_a?(Hash)
          image_params << "w#{options[:image_size][:width]}" if options[:image_size][:width]
          image_params << "h#{options[:image_size][:height]}" if options[:image_size][:height]
        end
        image_params << 'c' if options[:image_aspect_ratio] == 'square'

        '/' + image_params.join('-')
      end

      def strip_unnecessary_query_parameters(query_parameters)
        # strip `sz` parameter (defaults to sz=50) which overrides `image_size` options
        return nil if query_parameters.nil?

        params = CGI.parse(query_parameters)
        stripped_params = params.delete_if { |key| key == 'sz' }

        # don't return an empty Hash since that would result
        # in URLs with a trailing ? character: http://image.url?
        return nil if stripped_params.empty?

        URI.encode_www_form(stripped_params)
      end

      def token_info(access_token)
        return nil unless access_token

        @token_info ||= Hash.new do |h, k|
          h[k] = client.request(:get, 'https://www.googleapis.com/oauth2/v3/tokeninfo', params: { access_token: access_token }).parsed
        end

        @token_info[access_token]
      end

      def verify_token(access_token)
        return false unless access_token

        token_info = token_info(access_token)
        token_info['aud'] == options.client_id || options.authorized_client_ids.include?(token_info['aud'])
      end

      def verify_hd(access_token)
        return true unless options.hd

        @raw_info ||= access_token.get(USER_INFO_URL).parsed

        options.hd = options.hd.call if options.hd.is_a? Proc
        allowed_hosted_domains = Array(options.hd)

        raise CallbackError.new(:invalid_hd, 'Invalid Hosted Domain') unless allowed_hosted_domains.include?(@raw_info['hd']) || options.hd == '*'

        true
      end
    end
  end
end