Skip to content

PAM authentication fails when using HTTP Authorization with OAuth2 Client Credentials #3

@ThisIsMissEm

Description

@ThisIsMissEm

In OAuth 2.0, it's possible to authenticate the client when interacting with the token endpoint via HTTP Basic Authentication. It seems the devise_pam_authenticable2 module always runs, and it chokes because params[scope] is nil: https://github.com/devkral/devise_pam_authenticatable2/blob/master/lib/devise_pam_authenticatable/strategy.rb#L10-L11

I'm not sure why exactly devise is running the PAM authentication code for every route, and not just the authentication related routes, but that's perhaps a different issue.

The error that's generated looks like the following:

Failure/Error: @app.call(env)
     
     NoMethodError:
       undefined method '[]=' for nil
     # ./lib/mastodon/middleware/socket_cleanup.rb:11:in 'Mastodon::Middleware::SocketCleanup#call'
     # ./lib/mastodon/middleware/public_file_server.rb:20:in 'Mastodon::Middleware::PublicFileServer#call'
     # ./spec/support/signed_request_helpers.rb:23:in 'SignedRequestHelpers#post'
     # ./spec/requests/oauth/token_spec.rb:8:in 'block (3 levels) in <top (required)>'
     # ./spec/requests/oauth/token_spec.rb:101:in 'block (6 levels) in <top (required)>'
     # ./spec/rails_helper.rb:139:in 'block (2 levels) in <top (required)>'

The full stack trace is:

     Failure/Error: @app.call(env)

     NoMethodError:
       undefined method `[]=' for nil:NilClass
     # ./vendor/bundle/ruby/3.2.0/gems/devise_pam_authenticatable2-9.2.0/lib/devise_pam_authenticatable/strategy.rb:11:in `authenticate!'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/strategies/base.rb:55:in `_run!'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/proxy.rb:372:in `block in _run_strategies_for'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/proxy.rb:365:in `each'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/proxy.rb:365:in `_run_strategies_for'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/proxy.rb:335:in `_perform_authentication'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/proxy.rb:110:in `authenticate'
     # ./vendor/bundle/ruby/3.2.0/gems/devise-4.9.4/lib/devise/controllers/helpers.rb:128:in `current_user'
     # ./vendor/bundle/ruby/3.2.0/gems/active_model_serializers-0.10.15/lib/action_controller/serialization.rb:40:in `serialization_scope'
     # ./vendor/bundle/ruby/3.2.0/gems/active_model_serializers-0.10.15/lib/action_controller/serialization.rb:53:in `block in get_serializer'
     # ./vendor/bundle/ruby/3.2.0/gems/active_model_serializers-0.10.15/lib/action_controller/serialization.rb:53:in `fetch'
     # ./vendor/bundle/ruby/3.2.0/gems/active_model_serializers-0.10.15/lib/action_controller/serialization.rb:53:in `get_serializer'
     # ./vendor/bundle/ruby/3.2.0/gems/active_model_serializers-0.10.15/lib/action_controller/serialization.rb:71:in `block (2 levels) in <module:Serialization>'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/renderers.rb:148:in `block in _render_to_body_with_renderer'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/renderers.rb:144:in `_render_to_body_with_renderer'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/renderers.rb:140:in `render_to_body'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/abstract_controller/rendering.rb:28:in `render'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/rendering.rb:167:in `render'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:31:in `block (2 levels) in render'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/benchmark.rb:17:in `realtime'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:31:in `block in render'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:100:in `cleanup_view_runtime'
     # ./vendor/bundle/ruby/3.2.0/gems/activerecord-8.0.2/lib/active_record/railties/controller_runtime.rb:46:in `cleanup_view_runtime'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:30:in `render'
     # ./vendor/bundle/ruby/3.2.0/gems/doorkeeper-5.8.2/app/controllers/doorkeeper/tokens_controller.rb:13:in `create'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/basic_implicit_render.rb:8:in `send_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/abstract_controller/base.rb:226:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/rendering.rb:193:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/abstract_controller/callbacks.rb:261:in `block in process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/callbacks.rb:109:in `run_callbacks'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/abstract_controller/callbacks.rb:260:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/rescue.rb:27:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:76:in `block in process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/notifications.rb:210:in `block in instrument'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/notifications.rb:210:in `instrument'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/instrumentation.rb:75:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal/params_wrapper.rb:259:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/activerecord-8.0.2/lib/active_record/railties/controller_runtime.rb:39:in `process_action'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/abstract_controller/base.rb:163:in `process'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal.rb:252:in `dispatch'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_controller/metal.rb:335:in `dispatch'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/routing/route_set.rb:67:in `dispatch'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/routing/route_set.rb:50:in `serve'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/journey/router.rb:53:in `block in serve'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/journey/router.rb:133:in `block in find_routes'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/journey/router.rb:126:in `each'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/journey/router.rb:126:in `find_routes'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/journey/router.rb:34:in `serve'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/routing/route_set.rb:908:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/railties-8.0.2/lib/rails/engine/lazy_route_set.rb:68:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:472:in `call_app!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:314:in `mock_call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:189:in `call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:169:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:472:in `call_app!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:314:in `mock_call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:189:in `call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:169:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:472:in `call_app!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:314:in `mock_call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:189:in `call!'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/strategy.rb:169:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/omniauth-2.1.3/lib/omniauth/builder.rb:44:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-attack-6.7.0/lib/rack/attack.rb:103:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/http_accept_language-2.1.1/lib/http_accept_language/middleware.rb:14:in `call'
     # ./lib/mastodon/middleware/socket_cleanup.rb:11:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-attack-6.7.0/lib/rack/attack.rb:127:in `call'
     # ./lib/mastodon/middleware/public_file_server.rb:20:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/manager.rb:36:in `block in call'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/manager.rb:34:in `catch'
     # ./vendor/bundle/ruby/3.2.0/gems/warden-1.2.9/lib/warden/manager.rb:34:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/tempfile_reaper.rb:15:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/etag.rb:27:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/conditional_get.rb:40:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/head.rb:12:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/http/permissions_policy.rb:38:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/http/content_security_policy.rb:38:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/session/abstract/id.rb:266:in `context'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/session/abstract/id.rb:260:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/cookies.rb:706:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/callbacks.rb:31:in `block in call'
     # ./vendor/bundle/ruby/3.2.0/gems/activesupport-8.0.2/lib/active_support/callbacks.rb:100:in `run_callbacks'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/callbacks.rb:30:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/actionable_exceptions.rb:18:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/debug_exceptions.rb:31:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/show_exceptions.rb:32:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/chewy-7.6.0/lib/chewy/railtie.rb:21:in `block in call'
     # ./vendor/bundle/ruby/3.2.0/gems/chewy-7.6.0/lib/chewy/strategy.rb:60:in `wrap'
     # ./vendor/bundle/ruby/3.2.0/gems/chewy-7.6.0/lib/chewy.rb:151:in `strategy'
     # ./vendor/bundle/ruby/3.2.0/gems/chewy-7.6.0/lib/chewy/railtie.rb:21:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/railties-8.0.2/lib/rails/rack/logger.rb:41:in `call_app'
     # ./vendor/bundle/ruby/3.2.0/gems/railties-8.0.2/lib/rails/rack/logger.rb:29:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/remote_ip.rb:96:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/request_id.rb:34:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/method_override.rb:24:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/runtime.rb:22:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/middleware/executor.rb:16:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-2.2.13/lib/rack/sendfile.rb:110:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-cors-2.0.2/lib/rack/cors.rb:102:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/railties-8.0.2/lib/rails/engine.rb:535:in `call'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-test-2.2.0/lib/rack/test.rb:360:in `process_request'
     # ./vendor/bundle/ruby/3.2.0/gems/rack-test-2.2.0/lib/rack/test.rb:153:in `request'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/testing/integration.rb:297:in `process'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/testing/integration.rb:25:in `post'
     # ./vendor/bundle/ruby/3.2.0/gems/actionpack-8.0.2/lib/action_dispatch/testing/integration.rb:388:in `post'
     # ./spec/support/signed_request_helpers.rb:23:in `post'
     # ./spec/requests/oauth/token_spec.rb:8:in `block (3 levels) in <top (required)>'
     # ./spec/requests/oauth/token_spec.rb:78:in `block (5 levels) in <top (required)>'
     # ./spec/rails_helper.rb:139:in `block (2 levels) in <top (required)>'
     # ./vendor/bundle/ruby/3.2.0/gems/webmock-3.25.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # ./vendor/bundle/ruby/3.2.0/gems/thor-1.3.2/lib/thor/command.rb:28:in `run'
     # ./vendor/bundle/ruby/3.2.0/gems/thor-1.3.2/lib/thor/invocation.rb:127:in `invoke_command'
     # ./vendor/bundle/ruby/3.2.0/gems/thor-1.3.2/lib/thor.rb:538:in `dispatch'
     # ./vendor/bundle/ruby/3.2.0/gems/thor-1.3.2/lib/thor/base.rb:584:in `start'

I was able to add a binding.break point in, and could see why the code in this gem is failing:

Image

One option for a solution may be to change authenticate! in this gem to something like the following:

  def authenticate!
-    pam_params = params[scope].clone
-    pam_params[:request] = request
-    if (resource = mapping.to.authenticate_with_pam(pam_params))
+    if (resource = mapping.to.authenticate_with_pam(authentication_hash.merge(password: password, request: request)))
      success!(resource)
    else
      fail(:invalid)
    end
  end

Which is based on how devise ldap works: https://github.com/cschiewek/devise_ldap_authenticatable/blob/default/lib/devise_ldap_authenticatable/strategy.rb#L13

I suspect what's in authentication_hash is what you were after by looking at params[scope] because that's what valid_for_params_auth? under the hood in doorkeeper does: https://github.com/heartcombo/devise/blob/main/lib/devise/strategies/authenticatable.rb#L77-L80

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions