Module: SecondFactorManager

Extended by:
ActiveSupport::Concern
Included in:
User, UserSecondFactor
Defined in:
app/models/concerns/second_factor_manager.rb

Defined Under Namespace

Classes: SecondFactorAuthenticationResult

Constant Summary collapse

TOTP_ALLOWED_DRIFT_SECONDS =
30

Instance Method Summary collapse

Instance Method Details

#authenticate_backup_code(backup_code) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'app/models/concerns/second_factor_manager.rb', line 246

def authenticate_backup_code(backup_code)
  if !backup_code.blank?
    codes = self&.user_second_factors&.backup_codes

    codes.each do |code|
      parsed_data = JSON.parse(code.data)
      stored_code = parsed_data["code_hash"]
      stored_salt = parsed_data["salt"]
      backup_hash = hash_backup_code(backup_code, stored_salt)
      next unless backup_hash == stored_code

      code.update(enabled: false, last_used: DateTime.now)
      return true
    end
    false
  end
  false
end

#authenticate_second_factor(params, secure_session) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/models/concerns/second_factor_manager.rb', line 107

def authenticate_second_factor(params, secure_session)
  ok_result = SecondFactorAuthenticationResult.new(true)
  return ok_result if !security_keys_enabled? && !totp_or_backup_codes_enabled?

  second_factor_token = params[:second_factor_token]
  second_factor_method = params[:second_factor_method]&.to_i

  if second_factor_method.blank? || UserSecondFactor.methods[second_factor_method].blank?
    return invalid_second_factor_method_result
  end

  if !valid_second_factor_method_for_user?(second_factor_method)
    return not_enabled_second_factor_method_result
  end

  case second_factor_method
  when UserSecondFactor.methods[:totp]
    if authenticate_totp(second_factor_token)
      ok_result.used_2fa_method = UserSecondFactor.methods[:totp]
      return ok_result
    else
      return invalid_totp_or_backup_code_result
    end
  when UserSecondFactor.methods[:backup_codes]
    if authenticate_backup_code(second_factor_token)
      ok_result.used_2fa_method = UserSecondFactor.methods[:backup_codes]
      return ok_result
    else
      return invalid_totp_or_backup_code_result
    end
  when UserSecondFactor.methods[:security_key]
    if authenticate_security_key(secure_session, second_factor_token)
      ok_result.used_2fa_method = UserSecondFactor.methods[:security_key]
      return ok_result
    else
      return invalid_security_key_result
    end
  end

  # if we have gotten down to this point without being
  # OK or invalid something has gone very weird.
  invalid_second_factor_method_result
rescue ::DiscourseWebauthn::SecurityKeyError => err
  invalid_security_key_result(err.message)
end

#authenticate_security_key(secure_session, security_key_credential) ⇒ Object



165
166
167
168
169
170
171
172
173
# File 'app/models/concerns/second_factor_manager.rb', line 165

def authenticate_security_key(secure_session, security_key_credential)
  ::DiscourseWebauthn::SecurityKeyAuthenticationService.new(
    self,
    security_key_credential,
    challenge: DiscourseWebauthn.challenge(self, secure_session),
    rp_id: DiscourseWebauthn.rp_id,
    origin: Discourse.base_url,
  ).authenticate_security_key
end

#authenticate_totp(token) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'app/models/concerns/second_factor_manager.rb', line 40

def authenticate_totp(token)
  totps = self&.user_second_factors.totps
  authenticated = false
  totps.each do |totp|
    last_used = 0

    last_used = totp.last_used.to_i if totp.last_used

    authenticated =
      !token.blank? &&
        totp.totp_object.verify(
          token,
          drift_ahead: TOTP_ALLOWED_DRIFT_SECONDS,
          drift_behind: TOTP_ALLOWED_DRIFT_SECONDS,
          after: last_used,
        )

    if authenticated
      totp.update!(last_used: DateTime.now)
      break
    end
  end
  !!authenticated
end

#backup_codes_enabled?Boolean

Returns:

  • (Boolean)


70
71
72
73
# File 'app/models/concerns/second_factor_manager.rb', line 70

def backup_codes_enabled?
  !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins &&
    self&.user_second_factors.backup_codes.exists?
end

#create_backup_codes(codes) ⇒ Object



235
236
237
238
239
240
241
242
243
244
# File 'app/models/concerns/second_factor_manager.rb', line 235

def create_backup_codes(codes)
  codes.each do |code|
    UserSecondFactor.create!(
      user_id: self.id,
      data: code.to_json,
      enabled: true,
      method: UserSecondFactor.methods[:backup_codes],
    )
  end
end

#create_totp(opts = {}) ⇒ Object



20
21
22
23
24
25
26
27
28
29
# File 'app/models/concerns/second_factor_manager.rb', line 20

def create_totp(opts = {})
  require_rotp
  UserSecondFactor.create!(
    {
      user_id: self.id,
      method: UserSecondFactor.methods[:totp],
      data: ROTP::Base32.random,
    }.merge(opts),
  )
end

#generate_backup_codesObject



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'app/models/concerns/second_factor_manager.rb', line 215

def generate_backup_codes
  codes = []
  10.times { codes << SecureRandom.hex(16) }

  codes_json =
    codes.map do |code|
      salt = SecureRandom.hex(16)
      { salt: salt, code_hash: hash_backup_code(code, salt) }
    end

  if self.user_second_factors.backup_codes.empty?
    create_backup_codes(codes_json)
  else
    self.user_second_factors.where(method: UserSecondFactor.methods[:backup_codes]).destroy_all
    create_backup_codes(codes_json)
  end

  codes
end

#get_totp_object(data) ⇒ Object



31
32
33
34
# File 'app/models/concerns/second_factor_manager.rb', line 31

def get_totp_object(data)
  require_rotp
  ROTP::TOTP.new(data, issuer: SiteSetting.title.gsub(":", ""))
end

#has_any_second_factor_methods_enabled?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'app/models/concerns/second_factor_manager.rb', line 83

def has_any_second_factor_methods_enabled?
  totp_enabled? || security_keys_enabled?
end

#has_multiple_second_factor_methods?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'app/models/concerns/second_factor_manager.rb', line 87

def has_multiple_second_factor_methods?
  security_keys_enabled? && totp_or_backup_codes_enabled?
end

#hash_backup_code(code, salt) ⇒ Object



265
266
267
268
269
270
# File 'app/models/concerns/second_factor_manager.rb', line 265

def hash_backup_code(code, salt)
  # Backup codes have high entropy, so we can afford to use
  # a lower number of iterations than for user-specific passwords
  iterations = Rails.env.test? ? 10 : 64_000
  Pbkdf2.hash_password(code, salt, iterations, "sha256")
end

#invalid_second_factor_authentication_result(error_message, reason) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
# File 'app/models/concerns/second_factor_manager.rb', line 203

def invalid_second_factor_authentication_result(error_message, reason)
  SecondFactorAuthenticationResult.new(
    false,
    error_message,
    reason,
    backup_codes_enabled?,
    security_keys_enabled?,
    totp_enabled?,
    has_multiple_second_factor_methods?,
  )
end

#invalid_second_factor_method_resultObject



189
190
191
192
193
194
# File 'app/models/concerns/second_factor_manager.rb', line 189

def invalid_second_factor_method_result
  invalid_second_factor_authentication_result(
    I18n.t("login.invalid_second_factor_method"),
    "invalid_second_factor_method",
  )
end

#invalid_security_key_result(error_message = nil) ⇒ Object



182
183
184
185
186
187
# File 'app/models/concerns/second_factor_manager.rb', line 182

def invalid_security_key_result(error_message = nil)
  invalid_second_factor_authentication_result(
    error_message || I18n.t("login.invalid_security_key"),
    "invalid_security_key",
  )
end

#invalid_totp_or_backup_code_resultObject



175
176
177
178
179
180
# File 'app/models/concerns/second_factor_manager.rb', line 175

def invalid_totp_or_backup_code_result
  invalid_second_factor_authentication_result(
    I18n.t("login.invalid_second_factor_code"),
    "invalid_second_factor",
  )
end

#not_enabled_second_factor_method_resultObject



196
197
198
199
200
201
# File 'app/models/concerns/second_factor_manager.rb', line 196

def not_enabled_second_factor_method_result
  invalid_second_factor_authentication_result(
    I18n.t("login.not_enabled_second_factor_method"),
    "not_enabled_second_factor_method",
  )
end

#only_security_keys_enabled?Boolean

Returns:

  • (Boolean)


95
96
97
# File 'app/models/concerns/second_factor_manager.rb', line 95

def only_security_keys_enabled?
  security_keys_enabled? && !totp_or_backup_codes_enabled?
end

#only_totp_or_backup_codes_enabled?Boolean

Returns:

  • (Boolean)


99
100
101
# File 'app/models/concerns/second_factor_manager.rb', line 99

def only_totp_or_backup_codes_enabled?
  !security_keys_enabled? && totp_or_backup_codes_enabled?
end

#remaining_backup_codesObject



103
104
105
# File 'app/models/concerns/second_factor_manager.rb', line 103

def remaining_backup_codes
  self&.user_second_factors&.backup_codes&.count
end

#require_rotpObject



272
273
274
# File 'app/models/concerns/second_factor_manager.rb', line 272

def require_rotp
  require "rotp" if !defined?(ROTP)
end

#security_keys_enabled?Boolean

Returns:

  • (Boolean)


75
76
77
78
79
80
81
# File 'app/models/concerns/second_factor_manager.rb', line 75

def security_keys_enabled?
  !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins &&
    self
      &.security_keys
      .where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true)
      .exists?
end

#totp_enabled?Boolean

Returns:

  • (Boolean)


65
66
67
68
# File 'app/models/concerns/second_factor_manager.rb', line 65

def totp_enabled?
  !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins &&
    self&.user_second_factors.totps.exists?
end

#totp_or_backup_codes_enabled?Boolean

Returns:

  • (Boolean)


91
92
93
# File 'app/models/concerns/second_factor_manager.rb', line 91

def totp_or_backup_codes_enabled?
  totp_enabled? || backup_codes_enabled?
end

#totp_provisioning_uri(data) ⇒ Object



36
37
38
# File 'app/models/concerns/second_factor_manager.rb', line 36

def totp_provisioning_uri(data)
  get_totp_object(data).provisioning_uri(self.email)
end

#valid_second_factor_method_for_user?(method) ⇒ Boolean

Returns:

  • (Boolean)


153
154
155
156
157
158
159
160
161
162
163
# File 'app/models/concerns/second_factor_manager.rb', line 153

def valid_second_factor_method_for_user?(method)
  case method
  when UserSecondFactor.methods[:totp]
    return totp_enabled?
  when UserSecondFactor.methods[:backup_codes]
    return backup_codes_enabled?
  when UserSecondFactor.methods[:security_key]
    return security_keys_enabled?
  end
  false
end