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

mgmodell / devise_token_auth_multi_email / #1752

07 Apr 2026 03:12PM UTC coverage: 86.161% (+2.6%) from 83.553%
#1752

Pull #11

web-flow
Fix find_resource: check resource_class.respond_to?(:find_by_email) per-model, not gem-loaded globally

Agent-Logs-Url: https://github.com/mgmodell/devise_token_auth_multi_email/sessions/46c201bf-8c25-4bb2-8bcd-56557787d50d

Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com>
Pull Request #11: Fix Mongoid test failures: `find_resource` calls AR-only `find_by_email` on all models

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 2 files now uncovered.

1046 of 1214 relevant lines covered (86.16%)

269.49 hits per line

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

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

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

7
    before_action :validate_auth_origin_url_param
1✔
8

9
    skip_before_action :set_user_by_token, raise: false
1✔
10
    skip_after_action :update_auth_header
1✔
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
1✔
15

16
      # derive target redirect route from 'resource_class' param, which was set
17
      # before authentication.
18
      devise_mapping = get_devise_mapping
30✔
19
      redirect_route = get_redirect_route(devise_mapping)
30✔
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')
30✔
24
      session['dta.omniauth.params'] = request.env['omniauth.params']
30✔
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
30✔
31
      redirect_url = omniauth_env_params ?
30✔
32
        DeviseTokenAuth::Url.generate(redirect_route, omniauth_env_params) :
30✔
UNCOV
33
        redirect_route
×
34

35
      redirect_to redirect_url, {status: 307}.merge(redirect_options)
30✔
36
    end
37

38
    def get_redirect_route(devise_mapping)
1✔
39
      path = "#{Devise.mappings[devise_mapping.to_sym].fullpath}/#{params[:provider]}/callback"
30✔
40
      klass = request.scheme == 'https' ? URI::HTTPS : URI::HTTP
30✔
41
      redirect_route = klass.build(host: request.host, port: request.port, path: path).to_s
30✔
42
    end
43

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

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

66
    def omniauth_success
1✔
67
      get_resource_from_auth_hash
30✔
68
      set_token_on_resource
30✔
69
      create_auth_params
30✔
70

71
      if confirmable_enabled?
30✔
72
        # don't send confirmation email!!!
73
        @resource.skip_confirmation!
30✔
74
      end
75

76
      sign_in(:user, @resource, store: false, bypass: false)
30✔
77

78
      @resource.save!
30✔
79

80
      yield @resource if block_given?
30✔
81

82
      if DeviseTokenAuth.cookie_enabled
30✔
83
        set_token_in_cookie(@resource, @token)
×
84
      end
85

86
      render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)
30✔
87
    end
88

89
    def omniauth_failure
1✔
90
      @error = params[:message]
4✔
91
      render_data_or_redirect('authFailure', error: @error)
4✔
92
    end
93

94
    def validate_auth_origin_url_param
1✔
95
      return render_error_not_allowed_auth_origin_url if auth_origin_url && blacklisted_redirect_url?(auth_origin_url)
64✔
96
    end
97

98

99
    protected
1✔
100

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

142
    # break out provider attribute assignment for easy method extension
143
    def assign_provider_attrs(user, auth_hash)
1✔
144
      attrs = auth_hash['info'].to_hash
27✔
145
      attrs = attrs.slice(*user.attribute_names)
27✔
146
      user.assign_attributes(attrs)
27✔
147
    end
148

149
    # derive allowed params from the standard devise parameter sanitizer
150
    def whitelisted_params
1✔
151
      whitelist = params_for_resource(:sign_up)
30✔
152

153
      whitelist.inject({}) do |coll, key|
30✔
154
        param = omniauth_params[key.to_s]
180✔
155
        coll[key] = param if param
180✔
156
        coll
180✔
157
      end
158
    end
159

160
    def resource_class(mapping = nil)
1✔
161
      return @resource_class if defined?(@resource_class)
194✔
162

163
      constant_name = omniauth_params['resource_class'].presence || params['resource_class'].presence
67✔
164
      @resource_class = ObjectSpace.each_object(Class).detect { |cls| cls.to_s == constant_name && cls.pretty_print_inspect.starts_with?(constant_name) }
196,298✔
165
      raise 'No resource_class found' if @resource_class.nil?
67✔
166

167
      @resource_class
67✔
168
    end
169

170
    def resource_name
1✔
171
      resource_class
67✔
172
    end
173

174
    def unsafe_auth_origin_url
1✔
175
      omniauth_params['auth_origin_url'] || omniauth_params['origin']
394✔
176
    end
177

178

179
    def auth_origin_url
1✔
180
      if unsafe_auth_origin_url && blacklisted_redirect_url?(unsafe_auth_origin_url)
133✔
181
        return nil
3✔
182
      end
183
      return unsafe_auth_origin_url
130✔
184
    end
185

186
    # in the success case, omniauth_window_type is in the omniauth_params.
187
    # in the failure case, it is in a query param.  See monkey patch above
188
    def omniauth_window_type
1✔
189
      omniauth_params.nil? ? params['omniauth_window_type'] : omniauth_params['omniauth_window_type']
34✔
190
    end
191

192
    # this session value is set by the redirect_callbacks method. its purpose
193
    # is to persist the omniauth auth hash value thru a redirect. the value
194
    # must be destroyed immediately after it is accessed by omniauth_success
195
    def auth_hash
1✔
196
      @_auth_hash ||= session.delete('dta.omniauth.auth')
90✔
197
    end
198

199
    # ensure that this controller responds to :devise_controller? conditionals.
200
    # this is used primarily for access to the parameter sanitizers.
201
    def assert_is_devise_resource!
1✔
202
      true
64✔
203
    end
204

205
    def set_random_password
1✔
206
      # set crazy password for new oauth users. this is only used to prevent
207
      # access via email sign-in.
208
      p = SecureRandom.urlsafe_base64(nil, false)
29✔
209
      @resource.password = p
29✔
210
      @resource.password_confirmation = p
29✔
211
    end
212

213
    def create_auth_params
1✔
214
      @auth_params = {
215
        auth_token: @token.token,
30✔
216
        client_id:  @token.client,
217
        uid:        @resource.uid,
218
        expiry:     @token.expiry,
219
        config:     @config
220
      }
221
      @auth_params.merge!(oauth_registration: true) if @oauth_registration
30✔
222
      @auth_params
30✔
223
    end
224

225
    def set_token_on_resource
1✔
226
      @config = omniauth_params['config_name']
30✔
227
      @token  = @resource.create_token
30✔
228
    end
229

230
    def render_error_not_allowed_auth_origin_url
1✔
231
      message = I18n.t('devise_token_auth.omniauth.not_allowed_redirect_url', redirect_url: unsafe_auth_origin_url)
×
232
      render_data_or_redirect('authFailure', error: message)
×
233
    end
234

235
    def render_data(message, data)
1✔
236
      @data = data.merge(message: ActionController::Base.helpers.sanitize(message))
29✔
237
      render layout: nil, template: 'devise_token_auth/omniauth_external_window'
29✔
238
    end
239

240
    def render_data_or_redirect(message, data, user_data = {})
1✔
241

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

253
      elsif auth_origin_url # default to same-window implementation, which forwards back to auth_origin_url
5✔
254

255
        # build and redirect to destination url
256
        redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true).merge(redirect_options))
3✔
257
      else
258

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

267
    def fallback_render(text)
1✔
268
        render inline: %Q(
2✔
269

270
            <html>
271
                    <head></head>
272
                    <body>
273
                            #{ActionController::Base.helpers.sanitize(text)}
274
                    </body>
275
            </html>)
276
    end
277

278
    def handle_new_resource
1✔
279
      @oauth_registration = true
29✔
280
      set_random_password
29✔
281
    end
282

283
    def assign_whitelisted_params?
1✔
284
      true
30✔
285
    end
286

287
    def get_resource_from_auth_hash
1✔
288
      # find or create user by provider and provider uid
289
      @resource = resource_class.where(
30✔
290
        uid: auth_hash['uid'],
291
        provider: auth_hash['provider']
292
      ).first_or_initialize
293

294
      if @resource.new_record?
30✔
295
        handle_new_resource
29✔
296
      end
297

298
      # sync user info with provider, update/generate auth token
299
      assign_provider_attrs(@resource, auth_hash)
30✔
300

301
      # assign any additional (whitelisted) attributes
302
      if assign_whitelisted_params?
30✔
303
        extra_params = whitelisted_params
30✔
304
        @resource.assign_attributes(extra_params) if extra_params
30✔
305
      end
306

307
      @resource
30✔
308
    end
309
  end
310
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