Skip to content

Commit 6eed19b

Browse files
Gargronhiyuki2578
authored andcommitted
Fix authentication before 2FA challenge (mastodon#11943)
Regression from mastodon#11831
1 parent 9ca376a commit 6eed19b

7 files changed

Lines changed: 139 additions & 98 deletions

File tree

app/controllers/auth/sessions_controller.rb

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class Auth::SessionsController < Devise::SessionsController
88
skip_before_action :require_no_authentication, only: [:create]
99
skip_before_action :require_functional!
1010

11+
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
12+
1113
before_action :set_instance_presenter, only: [:new]
1214
before_action :set_body_classes
1315

@@ -20,22 +22,9 @@ def new
2022
end
2123

2224
def create
23-
self.resource = begin
24-
if user_params[:email].blank? && session[:otp_user_id].present?
25-
User.find(session[:otp_user_id])
26-
else
27-
warden.authenticate!(auth_options)
28-
end
29-
end
30-
31-
if resource.otp_required_for_login?
32-
if user_params[:otp_attempt].present? && session[:otp_user_id].present?
33-
authenticate_with_two_factor_via_otp(resource)
34-
else
35-
prompt_for_two_factor(resource)
36-
end
37-
else
38-
authenticate_and_respond(resource)
25+
super do |resource|
26+
remember_me(resource)
27+
flash.delete(:notice)
3928
end
4029
end
4130

@@ -49,6 +38,16 @@ def destroy
4938

5039
protected
5140

41+
def find_user
42+
if session[:otp_user_id]
43+
User.find(session[:otp_user_id])
44+
else
45+
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
46+
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
47+
user ||= User.find_for_authentication(email: user_params[:email])
48+
end
49+
end
50+
5251
def user_params
5352
params.require(:user).permit(:email, :password, :otp_attempt)
5453
end
@@ -71,17 +70,35 @@ def after_sign_out_path_for(_resource_or_scope)
7170
super
7271
end
7372

73+
def two_factor_enabled?
74+
find_user&.otp_required_for_login?
75+
end
76+
7477
def valid_otp_attempt?(user)
7578
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
7679
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
7780
rescue OpenSSL::Cipher::CipherError
7881
false
7982
end
8083

84+
def authenticate_with_two_factor
85+
user = self.resource = find_user
86+
87+
if user_params[:otp_attempt].present? && session[:otp_user_id]
88+
authenticate_with_two_factor_via_otp(user)
89+
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
90+
# If encrypted_password is blank, we got the user from LDAP or PAM,
91+
# so credentials are already valid
92+
93+
prompt_for_two_factor(user)
94+
end
95+
end
96+
8197
def authenticate_with_two_factor_via_otp(user)
8298
if valid_otp_attempt?(user)
8399
session.delete(:otp_user_id)
84-
authenticate_and_respond(user)
100+
remember_me(user)
101+
sign_in(user)
85102
else
86103
flash.now[:alert] = I18n.t('users.invalid_otp_token')
87104
prompt_for_two_factor(user)
@@ -90,16 +107,10 @@ def authenticate_with_two_factor_via_otp(user)
90107

91108
def prompt_for_two_factor(user)
92109
session[:otp_user_id] = user.id
110+
@body_classes = 'lighter'
93111
render :two_factor
94112
end
95113

96-
def authenticate_and_respond(user)
97-
sign_in(user)
98-
remember_me(user)
99-
100-
respond_with user, location: after_sign_in_path_for(user)
101-
end
102-
103114
private
104115

105116
def set_instance_presenter
@@ -112,11 +123,9 @@ def set_body_classes
112123

113124
def home_paths(resource)
114125
paths = [about_path]
115-
116126
if single_user_mode? && resource.is_a?(User)
117127
paths << short_account_path(username: resource.account)
118128
end
119-
120129
paths
121130
end
122131

app/models/concerns/ldap_authenticable.rb

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,50 @@
33
module LdapAuthenticable
44
extend ActiveSupport::Concern
55

6-
def ldap_setup(_attributes)
7-
self.confirmed_at = Time.now.utc
8-
self.admin = false
9-
self.external = true
6+
class_methods do
7+
def authenticate_with_ldap(params = {})
8+
ldap = Net::LDAP.new(ldap_options)
9+
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email])
1010

11-
save!
12-
end
11+
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
12+
ldap_get_user(user_info.first)
13+
end
14+
end
1315

14-
class_methods do
1516
def ldap_get_user(attributes = {})
1617
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
1718

1819
if resource.blank?
19-
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
20-
resource.ldap_setup(attributes)
20+
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc)
21+
resource.save!
2122
end
2223

2324
resource
2425
end
26+
27+
def ldap_options
28+
opts = {
29+
host: Devise.ldap_host,
30+
port: Devise.ldap_port,
31+
base: Devise.ldap_base,
32+
33+
auth: {
34+
method: :simple,
35+
username: Devise.ldap_bind_dn,
36+
password: Devise.ldap_password,
37+
},
38+
39+
connect_timeout: 10,
40+
}
41+
42+
if [:simple_tls, :start_tls].include?(Devise.ldap_method)
43+
opts[:encryption] = {
44+
method: Devise.ldap_method,
45+
tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.tap { |options| options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if Devise.ldap_tls_no_verify },
46+
}
47+
end
48+
49+
opts
50+
end
2551
end
2652
end

config/application.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
require_relative '../lib/paperclip/type_corrector'
1414
require_relative '../lib/mastodon/snowflake'
1515
require_relative '../lib/mastodon/version'
16-
require_relative '../lib/devise/ldap_authenticatable'
16+
require_relative '../lib/devise/two_factor_ldap_authenticatable'
17+
require_relative '../lib/devise/two_factor_pam_authenticatable'
1718

1819
Dotenv::Railtie.load
1920

config/initializers/devise.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,10 @@ def valid?
7171

7272
Devise.setup do |config|
7373
config.warden do |manager|
74-
manager.default_strategies(scope: :user).unshift :database_authenticatable
75-
manager.default_strategies(scope: :user).unshift :ldap_authenticatable if Devise.ldap_authentication
76-
manager.default_strategies(scope: :user).unshift :pam_authenticatable if Devise.pam_authentication
77-
78-
# We handle 2FA in our own sessions controller so this gets in the way
79-
manager.default_strategies(scope: :user).delete :two_factor_backupable
80-
manager.default_strategies(scope: :user).delete :two_factor_authenticatable
74+
manager.default_strategies(scope: :user).unshift :two_factor_ldap_authenticatable if Devise.ldap_authentication
75+
manager.default_strategies(scope: :user).unshift :two_factor_pam_authenticatable if Devise.pam_authentication
76+
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
77+
manager.default_strategies(scope: :user).unshift :two_factor_backupable
8178
end
8279

8380
# The secret key used by Devise. Devise uses this key to generate

lib/devise/ldap_authenticatable.rb

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'net/ldap'
4+
require 'devise/strategies/base'
5+
6+
module Devise
7+
module Strategies
8+
class TwoFactorLdapAuthenticatable < Base
9+
def valid?
10+
valid_params? && mapping.to.respond_to?(:authenticate_with_ldap)
11+
end
12+
13+
def authenticate!
14+
resource = mapping.to.authenticate_with_ldap(params[scope])
15+
16+
if resource && !resource.otp_required_for_login?
17+
success!(resource)
18+
else
19+
fail(:invalid)
20+
end
21+
end
22+
23+
protected
24+
25+
def valid_params?
26+
params[scope] && params[scope][:password].present?
27+
end
28+
end
29+
end
30+
end
31+
32+
Warden::Strategies.add(:two_factor_ldap_authenticatable, Devise::Strategies::TwoFactorLdapAuthenticatable)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require 'devise/strategies/base'
4+
5+
module Devise
6+
module Strategies
7+
class TwoFactorPamAuthenticatable < Base
8+
def valid?
9+
valid_params? && mapping.to.respond_to?(:authenticate_with_pam)
10+
end
11+
12+
def authenticate!
13+
resource = mapping.to.authenticate_with_pam(params[scope])
14+
15+
if resource && !resource.otp_required_for_login?
16+
success!(resource)
17+
else
18+
fail(:invalid)
19+
end
20+
end
21+
22+
protected
23+
24+
def valid_params?
25+
params[scope] && params[scope][:password].present?
26+
end
27+
end
28+
end
29+
end
30+
31+
Warden::Strategies.add(:two_factor_pam_authenticatable, Devise::Strategies::TwoFactorPamAuthenticatable)

0 commit comments

Comments
 (0)