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

mgmodell / devise_token_auth_multi_email / #492

06 Mar 2026 02:20AM UTC coverage: 90.641% (-0.6%) from 91.231%
#492

push

mgmodell
version bump and test flow update

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

9 existing lines in 3 files now uncovered.

1075 of 1186 relevant lines covered (90.64%)

347.0 hits per line

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

90.68
/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
      redirect_to redirect_route, {status: 307}.merge(redirect_options)
30✔
27
    end
28

29
    def get_redirect_route(devise_mapping)
1✔
30
      path = "#{Devise.mappings[devise_mapping.to_sym].fullpath}/#{params[:provider]}/callback"
30✔
31
      klass = request.scheme == 'https' ? URI::HTTPS : URI::HTTP
30✔
32
      redirect_route = klass.build(host: request.host, port: request.port, path: path).to_s
30✔
33
    end
34

35
    def get_devise_mapping
1✔
36
       # derive target redirect route from 'resource_class' param, which was set
37
       # before authentication.
38
       devise_mapping = [request.env['omniauth.params']['namespace_name'],
30✔
39
                         request.env['omniauth.params']['resource_class'].underscore.gsub('/', '_')].compact.join('_')
40
    rescue NoMethodError => err
41
      default_devise_mapping
×
42
    end
43

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

57
    def omniauth_success
1✔
58
      get_resource_from_auth_hash
30✔
59
      set_token_on_resource
30✔
60
      create_auth_params
30✔
61

62
      if confirmable_enabled?
30✔
63
        # don't send confirmation email!!!
64
        @resource.skip_confirmation!
30✔
65
      end
66

67
      sign_in(:user, @resource, store: false, bypass: false)
30✔
68

69
      @resource.save!
30✔
70

71
      yield @resource if block_given?
30✔
72

73
      if DeviseTokenAuth.cookie_enabled
30✔
74
        set_token_in_cookie(@resource, @token)
×
75
      end
76

77
      render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)
30✔
78
    end
79

80
    def omniauth_failure
1✔
81
      @error = params[:message]
4✔
82
      render_data_or_redirect('authFailure', error: @error)
4✔
83
    end
84

85
    def validate_auth_origin_url_param
1✔
86
      return render_error_not_allowed_auth_origin_url if auth_origin_url && blacklisted_redirect_url?(auth_origin_url)
64✔
87
    end
88

89

90
    protected
1✔
91

92
    # this will be determined differently depending on the action that calls
93
    # it. redirect_callbacks is called upon returning from successful omniauth
94
    # authentication, and the target params live in an omniauth-specific
95
    # request.env variable. this variable is then persisted thru the redirect
96
    # using our own dta.omniauth.params session var. the omniauth_success
97
    # method will access that session var and then destroy it immediately
98
    # after use.  In the failure case, finally, the omniauth params
99
    # are added as query params in our monkey patch to OmniAuth in engine.rb
100
    def omniauth_params
1✔
101
      unless defined?(@_omniauth_params)
737✔
102
        if request.env['omniauth.params'] && request.env['omniauth.params'].any?
64✔
103
          @_omniauth_params = request.env['omniauth.params']
30✔
104
        elsif session['dta.omniauth.params'] && session['dta.omniauth.params'].any?
64✔
105
          @_omniauth_params ||= session.delete('dta.omniauth.params')
30✔
106
          @_omniauth_params
107
        elsif params['omniauth_window_type']
4✔
UNCOV
108
          @_omniauth_params = params.slice('omniauth_window_type', 'auth_origin_url', 'resource_class', 'origin')
×
109
        else
110
          @_omniauth_params = {}
4✔
111
        end
112
      end
113
      @_omniauth_params
737✔
114
    end
115

116
    # break out provider attribute assignment for easy method extension
117
    def assign_provider_attrs(user, auth_hash)
1✔
118
      attrs = auth_hash['info'].to_hash
27✔
119
      attrs = attrs.slice(*user.attribute_names)
27✔
120
      user.assign_attributes(attrs)
27✔
121
    end
122

123
    # derive allowed params from the standard devise parameter sanitizer
124
    def whitelisted_params
1✔
125
      whitelist = params_for_resource(:sign_up)
30✔
126

127
      whitelist.inject({}) do |coll, key|
30✔
128
        param = omniauth_params[key.to_s]
180✔
129
        coll[key] = param if param
180✔
130
        coll
180✔
131
      end
132
    end
133

134
    def resource_class(mapping = nil)
1✔
135
      return @resource_class if defined?(@resource_class)
188✔
136

137
      constant_name = omniauth_params['resource_class'].presence || params['resource_class'].presence
64✔
138
      @resource_class = ObjectSpace.each_object(Class).detect { |cls| cls.to_s == constant_name && cls.pretty_print_inspect.starts_with?(constant_name) }
242,836✔
139
      raise 'No resource_class found' if @resource_class.nil?
64✔
140

141
      @resource_class
64✔
142
    end
143

144
    def resource_name
1✔
145
      resource_class
64✔
146
    end
147

148
    def unsafe_auth_origin_url
1✔
149
      omniauth_params['auth_origin_url'] || omniauth_params['origin']
196✔
150
    end
151

152

153
    def auth_origin_url
1✔
154
      if unsafe_auth_origin_url && blacklisted_redirect_url?(unsafe_auth_origin_url)
98✔
UNCOV
155
        return nil
×
156
      end
157
      return unsafe_auth_origin_url
98✔
158
    end
159

160
    # in the success case, omniauth_window_type is in the omniauth_params.
161
    # in the failure case, it is in a query param.  See monkey patch above
162
    def omniauth_window_type
1✔
163
      omniauth_params.nil? ? params['omniauth_window_type'] : omniauth_params['omniauth_window_type']
34✔
164
    end
165

166
    # this session value is set by the redirect_callbacks method. its purpose
167
    # is to persist the omniauth auth hash value thru a redirect. the value
168
    # must be destroyed immediately after it is accessed by omniauth_success
169
    def auth_hash
1✔
170
      @_auth_hash ||= session.delete('dta.omniauth.auth')
90✔
171
    end
172

173
    # ensure that this controller responds to :devise_controller? conditionals.
174
    # this is used primarily for access to the parameter sanitizers.
175
    def assert_is_devise_resource!
1✔
176
      true
64✔
177
    end
178

179
    def set_random_password
1✔
180
      # set crazy password for new oauth users. this is only used to prevent
181
      # access via email sign-in.
182
      p = SecureRandom.urlsafe_base64(nil, false)
29✔
183
      @resource.password = p
29✔
184
      @resource.password_confirmation = p
29✔
185
    end
186

187
    def create_auth_params
1✔
188
      @auth_params = {
189
        auth_token: @token.token,
30✔
190
        client_id:  @token.client,
191
        uid:        @resource.uid,
192
        expiry:     @token.expiry,
193
        config:     @config
194
      }
195
      @auth_params.merge!(oauth_registration: true) if @oauth_registration
30✔
196
      @auth_params
30✔
197
    end
198

199
    def set_token_on_resource
1✔
200
      @config = omniauth_params['config_name']
30✔
201
      @token  = @resource.create_token
30✔
202
    end
203

204
    def render_error_not_allowed_auth_origin_url
1✔
205
      message = I18n.t('devise_token_auth.omniauth.not_allowed_redirect_url', redirect_url: unsafe_auth_origin_url)
×
206
      render_data_or_redirect('authFailure', error: message)
×
207
    end
208

209
    def render_data(message, data)
1✔
UNCOV
210
      @data = data.merge(message: ActionController::Base.helpers.sanitize(message))
×
UNCOV
211
      render layout: nil, template: 'devise_token_auth/omniauth_external_window'
×
212
    end
213

214
    def render_data_or_redirect(message, data, user_data = {})
1✔
215

216
      # We handle inAppBrowser and newWindow the same, but it is nice
217
      # to support values in case people need custom implementations for each case
218
      # (For example, nbrustein does not allow new users to be created if logging in with
219
      # an inAppBrowser)
220
      #
221
      # See app/views/devise_token_auth/omniauth_external_window.html.erb to understand
222
      # why we can handle these both the same.  The view is setup to handle both cases
223
      # at the same time.
224
      if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
34✔
UNCOV
225
        render_data(message, user_data.merge(data))
×
226

227
      elsif auth_origin_url # default to same-window implementation, which forwards back to auth_origin_url
34✔
228

229
        # build and redirect to destination url
UNCOV
230
        redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true).merge(redirect_options))
×
231
      else
232

233
        # there SHOULD always be an auth_origin_url, but if someone does something silly
234
        # like coming straight to this url or refreshing the page at the wrong time, there may not be one.
235
        # In that case, just render in plain text the error message if there is one or otherwise
236
        # a generic message.
237
        fallback_render data[:error] || 'An error occurred'
34✔
238
      end
239
    end
240

241
    def fallback_render(text)
1✔
242
        render inline: %Q(
34✔
243

244
            <html>
245
                    <head></head>
246
                    <body>
247
                            #{ActionController::Base.helpers.sanitize(text)}
248
                    </body>
249
            </html>)
250
    end
251

252
    def handle_new_resource
1✔
253
      @oauth_registration = true
29✔
254
      set_random_password
29✔
255
    end
256

257
    def assign_whitelisted_params?
1✔
258
      true
30✔
259
    end
260

261
    def get_resource_from_auth_hash
1✔
262
      # find or create user by provider and provider uid
263
      @resource = resource_class.where(
30✔
264
        uid: auth_hash['uid'],
265
        provider: auth_hash['provider']
266
      ).first_or_initialize
267

268
      if @resource.new_record?
30✔
269
        handle_new_resource
29✔
270
      end
271

272
      # sync user info with provider, update/generate auth token
273
      assign_provider_attrs(@resource, auth_hash)
30✔
274

275
      # assign any additional (whitelisted) attributes
276
      if assign_whitelisted_params?
30✔
277
        extra_params = whitelisted_params
30✔
278
        @resource.assign_attributes(extra_params) if extra_params
30✔
279
      end
280

281
      @resource
30✔
282
    end
283
  end
284
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