• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mgmodell / devise_token_auth_multi_email / #25121

08 Apr 2026 05:05PM UTC coverage: 13.745% (-17.8%) from 31.5%
#25121

push

GitHub
Merge pull request #19 from mgmodell/copilot/write-tests-for-validators

229 of 1666 relevant lines covered (13.75%)

0.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/app/controllers/devise_token_auth/omniauth_callbacks_controller.rb
1
# frozen_string_literal: true
2

3
module DeviseTokenAuth
×
4
  class OmniauthCallbacksController < DeviseTokenAuth::ApplicationController
×
5
    attr_reader :auth_params
×
6

7
    before_action :validate_auth_origin_url_param
×
8

9
    skip_before_action :set_user_by_token, raise: false
×
10
    skip_after_action :update_auth_header
×
11

12
    # intermediary route for successful omniauth authentication. omniauth does
13
    # not support multiple models, so we must resort to this terrible hack.
14
    def redirect_callbacks
×
15

16
      # derive target redirect route from 'resource_class' param, which was set
17
      # before authentication.
18
      devise_mapping = get_devise_mapping
×
19
      redirect_route = get_redirect_route(devise_mapping)
×
20

21
      # preserve omniauth info for success route. ignore 'extra' in twitter
22
      # auth response to avoid CookieOverflow.
23
      session['dta.omniauth.auth'] = request.env['omniauth.auth'].except('extra')
×
24
      session['dta.omniauth.params'] = request.env['omniauth.params']
×
25

26
      # Also encode omniauth params in the redirect URL so they survive as
27
      # request params in omniauth_success even when the session cookie is
28
      # not reliably forwarded through a 307 redirect chain (e.g. Rails 7.2+
29
      # integration tests where follow_redirect! preserves the POST method).
30
      omniauth_env_params = request.env['omniauth.params'].presence
×
31
      redirect_url = omniauth_env_params ?
×
32
        DeviseTokenAuth::Url.generate(redirect_route, omniauth_env_params) :
×
33
        redirect_route
×
34

35
      # Use 302/303 so the callback is performed as a GET and params/session
36
      # survive reliably across the redirect chain in Rails integration tests.
37
      # 303 is semantically "see other" after a POST; 302 is also widely used.
38
      # Suggested by Claude Sonnet 4
39
      redirect_to redirect_url, { status: 303 }.merge(redirect_options)
×
40
    end
×
41

42
    def get_redirect_route(devise_mapping)
×
43
      path = "#{Devise.mappings[devise_mapping.to_sym].fullpath}/#{params[:provider]}/callback"
×
44
      klass = request.scheme == 'https' ? URI::HTTPS : URI::HTTP
×
45
      redirect_route = klass.build(host: request.host, port: request.port, path: path).to_s
×
46
    end
×
47

48
    def get_devise_mapping
×
49
       # derive target redirect route from 'resource_class' param, which was set
50
       # before authentication.
51
       devise_mapping = [request.env['omniauth.params']['namespace_name'],
×
52
                         request.env['omniauth.params']['resource_class'].underscore.gsub('/', '_')].compact.join('_')
×
53
    rescue NoMethodError => err
×
54
      default_devise_mapping
×
55
    end
×
56

57
    # This method will only be called if `get_devise_mapping` cannot
58
    # find the mapping in `omniauth.params`.
59
    #
60
    # One example use-case here is for IDP-initiated SAML login.  In that
61
    # case, there will have been no initial request in which to save
62
    # the devise mapping.  If you are in a situation like that, and
63
    # your app allows for you to determine somehow what the devise
64
    # mapping should be (because, for example, it is always the same),
65
    # then you can handle it by overriding this method.
66
    def default_devise_mapping
×
67
      raise NotImplementedError.new('no default_devise_mapping set')
×
68
    end
×
69

70
    def omniauth_success
×
71
      get_resource_from_auth_hash
×
72
      set_token_on_resource
×
73
      create_auth_params
×
74

75
      if confirmable_enabled?
×
76
        # don't send confirmation email!!!
77
        @resource.skip_confirmation!
×
78
      end
×
79

80
      sign_in(:user, @resource, store: false, bypass: false)
×
81

82
      @resource.save!
×
83

84
      yield @resource if block_given?
×
85

86
      if DeviseTokenAuth.cookie_enabled
×
87
        set_token_in_cookie(@resource, @token)
×
88
      end
×
89

90
      render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)
×
91
    end
×
92

93
    def omniauth_failure
×
94
      @error = params[:message]
×
95
      render_data_or_redirect('authFailure', error: @error)
×
96
    end
×
97

98
    def validate_auth_origin_url_param
×
99
      return render_error_not_allowed_auth_origin_url if auth_origin_url && blacklisted_redirect_url?(auth_origin_url)
×
100
    end
×
101

102

103
    protected
×
104

105
    # this will be determined differently depending on the action that calls
106
    # it. redirect_callbacks is called upon returning from successful omniauth
107
    # authentication, and the target params live in an omniauth-specific
108
    # request.env variable. this variable is then persisted thru the redirect
109
    # using our own dta.omniauth.params session var. the omniauth_success
110
    # method will access that session var and then destroy it immediately
111
    # after use.  In the failure case, finally, the omniauth params
112
    # are added as query params in our monkey patch to OmniAuth in engine.rb
113
    def omniauth_params
×
114
      unless defined?(@_omniauth_params)
×
115
        if request.env['omniauth.params'] && request.env['omniauth.params'].any?
×
116
          @_omniauth_params = request.env['omniauth.params']
×
117
        elsif session['dta.omniauth.params'] && session['dta.omniauth.params'].any?
×
118
          @_omniauth_params = session['dta.omniauth.params']
×
119
        elsif params['omniauth_window_type'] || params['auth_origin_url']
×
120
          # Fallback: params may arrive as URL query string when the session
121
          # is not reliably forwarded through a 307 redirect chain (Rails 7.2+).
122
          # Pre-set @_omniauth_params to {} BEFORE calling params_for_resource to
123
          # break a potential recursive loop:
124
          #   omniauth_params → params_for_resource → devise_parameter_sanitizer
125
          #   → resource_class → omniauth_params (still computing!) → loop
126
          # With @_omniauth_params = {} set early, any re-entrant call returns {}
127
          # and resource_class falls back to params['resource_class'] directly.
128
          @_omniauth_params = {}
×
129
          omniauth_known_keys = %w[omniauth_window_type auth_origin_url resource_class
×
130
                                   origin namespace_name config_name]
×
131
          begin
×
132
            sign_up_keys = params_for_resource(:sign_up).map(&:to_s)
×
133
          rescue NoMethodError, TypeError
×
134
            sign_up_keys = []
×
135
          end
×
136
          @_omniauth_params = params.permit(*omniauth_known_keys, *sign_up_keys)
×
137
        else
×
138
          @_omniauth_params = {}
×
139
        end
×
140
        # Always clean up the session key, regardless of which branch was taken.
141
        session.delete('dta.omniauth.params')
×
142
      end
×
143
      @_omniauth_params
×
144
    end
×
145

146
    # break out provider attribute assignment for easy method extension
147
    def assign_provider_attrs(user, auth_hash)
×
148
      attrs = auth_hash['info'].to_hash
×
149
      attrs = attrs.slice(*user.attribute_names)
×
150
      user.assign_attributes(attrs)
×
151
    end
×
152

153
    # derive allowed params from the standard devise parameter sanitizer
154
    def whitelisted_params
×
155
      whitelist = params_for_resource(:sign_up)
×
156

157
      whitelist.inject({}) do |coll, key|
×
158
        param = omniauth_params[key.to_s]
×
159
        coll[key] = param if param
×
160
        coll
×
161
      end
×
162
    end
×
163

164
    def resource_class(mapping = nil)
×
165
      return @resource_class if defined?(@resource_class)
×
166

167
      constant_name = omniauth_params['resource_class'].presence || params['resource_class'].presence
×
168
      @resource_class = ObjectSpace.each_object(Class).detect { |cls| cls.to_s == constant_name && cls.pretty_print_inspect.starts_with?(constant_name) }
×
169
      raise 'No resource_class found' if @resource_class.nil?
×
170

171
      @resource_class
×
172
    end
×
173

174
    def resource_name
×
175
      resource_class
×
176
    end
×
177

178
    def unsafe_auth_origin_url
×
179
      omniauth_params['auth_origin_url'] || omniauth_params['origin']
×
180
    end
×
181

182

183
    def auth_origin_url
×
184
      if unsafe_auth_origin_url && blacklisted_redirect_url?(unsafe_auth_origin_url)
×
185
        return nil
×
186
      end
×
187
      return unsafe_auth_origin_url
×
188
    end
×
189

190
    # in the success case, omniauth_window_type is in the omniauth_params.
191
    # in the failure case, it is in a query param.  See monkey patch above
192
    def omniauth_window_type
×
193
      omniauth_params.nil? ? params['omniauth_window_type'] : omniauth_params['omniauth_window_type']
×
194
    end
×
195

196
    # this session value is set by the redirect_callbacks method. its purpose
197
    # is to persist the omniauth auth hash value thru a redirect. the value
198
    # must be destroyed immediately after it is accessed by omniauth_success
199
    def auth_hash
×
200
      @_auth_hash ||= session.delete('dta.omniauth.auth')
×
201
    end
×
202

203
    # ensure that this controller responds to :devise_controller? conditionals.
204
    # this is used primarily for access to the parameter sanitizers.
205
    def assert_is_devise_resource!
×
206
      true
×
207
    end
×
208

209
    def set_random_password
×
210
      # set crazy password for new oauth users. this is only used to prevent
211
      # access via email sign-in.
212
      p = SecureRandom.urlsafe_base64(nil, false)
×
213
      @resource.password = p
×
214
      @resource.password_confirmation = p
×
215
    end
×
216

217
    def create_auth_params
×
218
      @auth_params = {
×
219
        auth_token: @token.token,
×
220
        client_id:  @token.client,
×
221
        uid:        @resource.uid,
×
222
        expiry:     @token.expiry,
×
223
        config:     @config
×
224
      }
×
225
      @auth_params.merge!(oauth_registration: true) if @oauth_registration
×
226
      @auth_params
×
227
    end
×
228

229
    def set_token_on_resource
×
230
      @config = omniauth_params['config_name']
×
231
      @token  = @resource.create_token
×
232
    end
×
233

234
    def render_error_not_allowed_auth_origin_url
×
235
      message = I18n.t('devise_token_auth.omniauth.not_allowed_redirect_url', redirect_url: unsafe_auth_origin_url)
×
236
      render_data_or_redirect('authFailure', error: message)
×
237
    end
×
238

239
    def render_data(message, data)
×
240
      @data = data.merge(message: ActionController::Base.helpers.sanitize(message))
×
241
      render layout: nil, template: 'devise_token_auth/omniauth_external_window'
×
242
    end
×
243

244
    def render_data_or_redirect(message, data, user_data = {})
×
245

246
      # We handle inAppBrowser and newWindow the same, but it is nice
247
      # to support values in case people need custom implementations for each case
248
      # (For example, nbrustein does not allow new users to be created if logging in with
249
      # an inAppBrowser)
250
      #
251
      # See app/views/devise_token_auth/omniauth_external_window.html.erb to understand
252
      # why we can handle these both the same.  The view is setup to handle both cases
253
      # at the same time.
254
      if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
×
255
        render_data(message, user_data.merge(data))
×
256

257
      elsif auth_origin_url # default to same-window implementation, which forwards back to auth_origin_url
×
258

259
        # build and redirect to destination url
260
        redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true).merge(redirect_options))
×
261
      else
×
262

263
        # there SHOULD always be an auth_origin_url, but if someone does something silly
264
        # like coming straight to this url or refreshing the page at the wrong time, there may not be one.
265
        # In that case, just render in plain text the error message if there is one or otherwise
266
        # a generic message.
267
        fallback_render data[:error] || 'An error occurred'
×
268
      end
×
269
    end
×
270

271
    def fallback_render(text)
×
272
        render inline: %Q(
×
273

274
            <html>
×
275
                    <head></head>
×
276
                    <body>
×
277
                            #{ActionController::Base.helpers.sanitize(text)}
278
                    </body>
×
279
            </html>)
×
280
    end
×
281

282
    def handle_new_resource
×
283
      @oauth_registration = true
×
284
      set_random_password
×
285
    end
×
286

287
    def assign_whitelisted_params?
×
288
      true
×
289
    end
×
290

291
    def get_resource_from_auth_hash
×
292
      # find or create user by provider and provider uid
293
      @resource = resource_class.where(
×
294
        uid: auth_hash['uid'],
×
295
        provider: auth_hash['provider']
×
296
      ).first_or_initialize
×
297

298
      if @resource.new_record?
×
299
        handle_new_resource
×
300
      end
×
301

302
      # sync user info with provider, update/generate auth token
303
      assign_provider_attrs(@resource, auth_hash)
×
304

305
      # assign any additional (whitelisted) attributes
306
      if assign_whitelisted_params?
×
307
        extra_params = whitelisted_params
×
308
        @resource.assign_attributes(extra_params) if extra_params
×
309
      end
×
310

311
      @resource
×
312
    end
×
313
  end
×
314
end
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc