Skip to content

Security issue: database_authenticatable is implicitly added by concern, allowing 2FA bypass with devise-two-factor #1682

Description

@fillipeppalhares

Security issue

When using DeviseTokenAuth::Concerns::User together with devise-two-factor, the concern implicitly enables :database_authenticatable, which can lead to a 2FA bypass vulnerability.

Context

  • Devise-based application
  • Using devise_token_auth
  • Adding 2FA via devise-two-factor

Example model:

class User < ApplicationRecord
  include DeviseTokenAuth::Concerns::User

  devise :two_factor_authenticatable, :two_factor_backupable,
         otp_backup_code_length: 10,
         otp_number_of_backup_codes: 10

  devise :confirmable, :recoverable, :trackable,
         :timeoutable, :lockable
end

Problem

The concern contains the following logic:

    # Hack to check if devise is already enabled
    if method_defined?(:devise_modules)
      devise_modules.delete(:omniauthable)
    else
      devise :database_authenticatable, :registerable,
             :recoverable, :validatable, :confirmable
    end

Because concerns are typically included at the top of the model, devise_modules is not yet defined at that point, so it always falls into the else branch.

As a result, :database_authenticatable is implicitly added, regardless of the developer’s intended configuration.

Relying on developers to include the concern after calling devise is not safe, since:

  • it goes against common Rails conventions (concerns are usually declared at the top)
  • this requirement is not explicitly documented

Security impact

According to devise-two-factor documentation:

Loading both :database_authenticatable and :two_factor_authenticatable in a model is a security issue. It will allow users to bypass two-factor authentication regardless of how otp_required_for_login is set due to the way Warden handles cascading strategies!

This means the final configuration becomes:

User.devise_modules
# => [:database_authenticatable, :two_factor_authenticatable, ...]

This creates a silent insecure state, where:

  • The application appears correctly configured
  • 2FA is enabled
  • But authentication can bypass 2FA entirely

Root cause

The concern makes an irreversible authentication decision before the model's Devise configuration is fully defined. This breaks composability with other Devise extensions and prevents detecting conflicts like two_factor_authenticatable.

Suggested approaches

Some possible directions:

  1. Do not implicitly include :database_authenticatable
  • Let the application define authentication modules explicitly
  1. Allow configuration of default Devise modules
  • e.g. via initializer
  1. Detect and prevent unsafe combinations
  • Raise or warn if both :database_authenticatable and :two_factor_authenticatable are present

Contribution

I’d be happy to open a PR to address this. Before doing so, I’d like guidance on which approach aligns best with the project’s direction.

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