heartcombo/devise · error · ArgumentError

Mapping omniauth_callbacks on a resource that is not omniaut

Error message

Mapping omniauth_callbacks on a resource that is not omniauthable
Please add `devise :omniauthable` to the `#{mapping.class_name}` model

What it means

When devise_for is given controllers: { omniauth_callbacks: "..." }, Devise validates at route-draw time that the mapped model actually includes the :omniauthable module (mapping.omniauthable?). The omniauth_callbacks controller only makes sense for an omniauthable resource — its routes (/users/auth/:provider, callback routes) are generated from the model's declared omniauth_providers. If the model lacks devise :omniauthable, route drawing raises ArgumentError immediately at boot with the model class name in the message.

Source

Thrown at lib/devise/rails/routes.rb:258

      resources.each do |resource|
        mapping = Devise.add_mapping(resource, options)

        begin
          raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise)
        rescue NameError => e
          raise unless mapping.class_name == resource.to_s.classify
          warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \
            "no model #{mapping.class_name} defined in your application"
          next
        rescue NoMethodError => e
          raise unless e.message.include?("undefined method `devise'")
          raise_no_devise_method_error!(mapping.class_name)
        end

        if options[:controllers] && options[:controllers][:omniauth_callbacks]
          unless mapping.omniauthable?
            raise ArgumentError, "Mapping omniauth_callbacks on a resource that is not omniauthable\n" \
              "Please add `devise :omniauthable` to the `#{mapping.class_name}` model"
          end
        end

        routes = mapping.used_routes

        devise_scope mapping.name do
          with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
            routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
          end
        end
      end
    end

    # Allow you to add authentication request from the router.
    # Takes an optional scope and block to provide constraints
    # on the model instance itself.
    #

View on GitHub (pinned to 372b295fe6)

Solutions

  1. Add the module to the model together with its providers: devise :omniauthable, omniauth_providers: [:google_oauth2] in app/models/user.rb (plus the matching config.omniauth entries and omniauth-* gems), then restart.
  2. If OAuth is not actually used, remove the omniauth_callbacks entry (or the whole controllers: hash if it is the only override) from devise_for in config/routes.rb.
  3. If you are mid-migration, comment out the controllers override until the model module and provider gems are in place, then restore it in the same commit.
  4. Verify after boot with rails routes | grep omniauth_callbacks that the provider routes now exist.

Example fix

# before
# config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :recoverable
  # -> ArgumentError: resource is not omniauthable
end

# after
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :recoverable,
         :omniauthable, omniauth_providers: [:google_oauth2]
end

# Gemfile (provider gem must also be present)
gem "omniauth-google-oauth2"
Defensive patterns

Strategy: validation

Validate before calling

# Guard the route declaration against the model's actual modules (routes.rb):
devise_for :users,
  controllers: (User.devise_modules.include?(:omniauthable) ?
    { omniauth_callbacks: "users/omniauth_callbacks" } : {})

# Or in a boot check after routes load:
Rails.application.config.after_initialize do
  Devise.mappings.each_value do |m|
    next unless m.controllers.key?(:omniauth_callbacks)
    raise "#{m.class_name} needs devise :omniauthable" unless m.omniauthable?
  end
end

Type guard

def omniauthable_resource?(klass)
  klass.respond_to?(:devise_modules) && klass.devise_modules.include?(:omniauthable)
end

Prevention

When it happens

Trigger: config/routes.rb contains devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" } while app/models/user.rb only has devise :database_authenticatable, :registerable, :recoverable (no :omniauthable); the :omniauthable module was removed during a refactor but the controllers hash in routes.rb was left in place; routes were copied from an OAuth-enabled app into one whose model was never updated.

Common situations: Incrementally enabling OAuth (developer wires routes/controller first, model second); cleaning up unused Devise modules from a model without checking routes.rb; copying devise_for lines between apps or engines; merging branch A's routes with branch B's model.

Related errors


AI-assisted analysis of heartcombo/devise@372b295fe6 (2026-08-21). Data as JSON: /api/errors/bb84bbfe710253b1. Report an issue: GitHub.