lynndylanhurley / lynndylanhurley/devise_token_auth
Security issue: database_authenticatable is implicitly added by concern, allowing 2FA bypass with devise-two-factor
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 3.6k
- Forks
- 1.1k
- PR merge metrics
- No merged PRs in 30d
Description
## Security issue
When using `DeviseTokenAuth::Concerns::User` together with [devise-two-factor](https://github.com/devise-two-factor/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:
```ruby
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:
```rb
# 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:
```rb
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
2. Allow configuration of default Devise modules
- e.g. via initializer
3. 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at DeviseTokenAuth::Concerns::User and inspect the devise_modules branch shown in the issue. Reproduce the model configuration with devise-two-factor, then seek maintainer guidance on which proposed approach to implement. Done means the concern no longer silently creates the unsafe database_authenticatable and two_factor_authenticatable combination.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rails, ruby
- Domain
- authentication, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100