FIX: store information about the login method in the database. (#28054)

Previously in these 2 PRs, we introduced a new site setting `SiteSetting.enforce_second_factor_on_external_auth`.

https://github.com/discourse/discourse/pull/27547
https://github.com/discourse/discourse/pull/27674

When disabled, it should enforce 2FA for local login with username and password and skip the requirement when authenticating with oauth2.

We stored information about the login method in a secure session but it is not reliable. Therefore, information about the login method is moved to the database.
This commit is contained in:
Krzysztof Kotlarek 2024-07-24 17:19:58 +10:00 committed by GitHub
parent 0c13c91f84
commit b64d01bc10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 33 additions and 23 deletions

View File

@ -609,7 +609,7 @@ class ApplicationController < ActionController::Base
def login_method def login_method
return if current_user.anonymous? return if current_user.anonymous?
secure_session["oauth"] == "true" ? Auth::LOGIN_METHOD_OAUTH : Auth::LOGIN_METHOD_LOCAL current_user.authenticated_with_oauth ? Auth::LOGIN_METHOD_OAUTH : Auth::LOGIN_METHOD_LOCAL
end end
private private

View File

@ -370,7 +370,6 @@ class SessionController < ApplicationController
return render(json: @second_factor_failure_payload) if !second_factor_auth_result.ok return render(json: @second_factor_failure_payload) if !second_factor_auth_result.ok
if user.active && user.email_confirmed? if user.active && user.email_confirmed?
secure_session["oauth"] = false if !SiteSetting.persistent_sessions
login(user, second_factor_auth_result) login(user, second_factor_auth_result)
else else
not_activated(user) not_activated(user)

View File

@ -86,7 +86,6 @@ class Users::OmniauthCallbacksController < ApplicationController
cookies["_bypass_cache"] = true cookies["_bypass_cache"] = true
cookies[:authentication_data] = { value: client_hash.to_json, path: Discourse.base_path("/") } cookies[:authentication_data] = { value: client_hash.to_json, path: Discourse.base_path("/") }
secure_session.set("oauth", true, expires: SiteSetting.maximum_session_age.hours)
redirect_to @origin redirect_to @origin
end end
@ -183,7 +182,7 @@ class Users::OmniauthCallbacksController < ApplicationController
return return
end end
log_on_user(user) log_on_user(user, { authenticated_with_oauth: true })
Invite.invalidate_for_email(user.email) # invite link can't be used to log in anymore Invite.invalidate_for_email(user.email) # invite link can't be used to log in anymore
session[:authentication] = nil # don't carry around old auth info, perhaps move elsewhere session[:authentication] = nil # don't carry around old auth info, perhaps move elsewhere
@auth_result.authenticated = true @auth_result.authenticated = true

View File

@ -253,6 +253,9 @@ class User < ActiveRecord::Base
# Cache for user custom fields. Currently it is used to display quick search results # Cache for user custom fields. Currently it is used to display quick search results
attr_accessor :custom_data attr_accessor :custom_data
# Information if user was authenticated with OAuth
attr_accessor :authenticated_with_oauth
scope :with_email, scope :with_email,
->(email) { joins(:user_emails).where("lower(user_emails.email) IN (?)", email) } ->(email) { joins(:user_emails).where("lower(user_emails.email) IN (?)", email) }

View File

@ -78,7 +78,8 @@ class UserAuthToken < ActiveRecord::Base
client_ip: nil, client_ip: nil,
path: nil, path: nil,
staff: nil, staff: nil,
impersonate: false impersonate: false,
authenticated_with_oauth: false
) )
token = SecureRandom.hex(16) token = SecureRandom.hex(16)
hashed_token = hash_token(token) hashed_token = hash_token(token)
@ -90,6 +91,7 @@ class UserAuthToken < ActiveRecord::Base
auth_token: hashed_token, auth_token: hashed_token,
prev_auth_token: hashed_token, prev_auth_token: hashed_token,
rotated_at: Time.zone.now, rotated_at: Time.zone.now,
authenticated_with_oauth: !!authenticated_with_oauth,
) )
user_auth_token.unhashed_auth_token = token user_auth_token.unhashed_auth_token = token
@ -278,17 +280,18 @@ end
# #
# Table name: user_auth_tokens # Table name: user_auth_tokens
# #
# id :integer not null, primary key # id :integer not null, primary key
# user_id :integer not null # user_id :integer not null
# auth_token :string not null # auth_token :string not null
# prev_auth_token :string not null # prev_auth_token :string not null
# user_agent :string # user_agent :string
# auth_token_seen :boolean default(FALSE), not null # auth_token_seen :boolean default(FALSE), not null
# client_ip :inet # client_ip :inet
# rotated_at :datetime not null # rotated_at :datetime not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# seen_at :datetime # seen_at :datetime
# authenticated_with_oauth :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLoginMethodToUserAuthTokens < ActiveRecord::Migration[7.1]
def change
add_column :user_auth_tokens, :authenticated_with_oauth, :boolean, default: false
end
end

View File

@ -140,6 +140,7 @@ class Auth::DefaultCurrentUserProvider
end end
current_user = @user_token.try(:user) current_user = @user_token.try(:user)
current_user.authenticated_with_oauth = @user_token.authenticated_with_oauth if current_user
end end
if !current_user if !current_user
@ -267,6 +268,7 @@ class Auth::DefaultCurrentUserProvider
client_ip: @request.ip, client_ip: @request.ip,
staff: user.staff?, staff: user.staff?,
impersonate: opts[:impersonate], impersonate: opts[:impersonate],
authenticated_with_oauth: opts[:authenticated_with_oauth],
) )
set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar)

View File

@ -152,8 +152,8 @@ RSpec.describe ApplicationController do
it "should redirect users when enforce_second_factor is 'all' and authenticated via oauth" do it "should redirect users when enforce_second_factor is 'all' and authenticated via oauth" do
SiteSetting.enforce_second_factor = "all" SiteSetting.enforce_second_factor = "all"
write_secure_session("oauth", true)
sign_in(user) sign_in(user)
user.user_auth_tokens.last.update(authenticated_with_oauth: true)
get "/" get "/"
expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor") expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor")
@ -162,8 +162,8 @@ RSpec.describe ApplicationController do
it "should not redirect users when enforce_second_factor is 'all', authenticated via oauth but enforce_second_factor_on_external_auth is false" do it "should not redirect users when enforce_second_factor is 'all', authenticated via oauth but enforce_second_factor_on_external_auth is false" do
SiteSetting.enforce_second_factor = "all" SiteSetting.enforce_second_factor = "all"
SiteSetting.enforce_second_factor_on_external_auth = false SiteSetting.enforce_second_factor_on_external_auth = false
write_secure_session("oauth", true)
sign_in(user) sign_in(user)
user.user_auth_tokens.last.update(authenticated_with_oauth: true)
get "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)

View File

@ -236,11 +236,6 @@ RSpec.describe Users::OmniauthCallbacksController do
expect(data["email_valid"]).to eq(true) expect(data["email_valid"]).to eq(true)
expect(data["can_edit_username"]).to eq(true) expect(data["can_edit_username"]).to eq(true)
expect(data["destination_url"]).to eq(destination_url) expect(data["destination_url"]).to eq(destination_url)
expect(read_secure_session["oauth"]).to eq("true")
expect(Discourse.redis.ttl("#{session[:secure_session_id]}oauth")).to be_between(
SiteSetting.maximum_session_age.hours.seconds - 10,
SiteSetting.maximum_session_age.hours.seconds,
)
end end
it "should return the right response for staged users" do it "should return the right response for staged users" do
@ -402,6 +397,7 @@ RSpec.describe Users::OmniauthCallbacksController do
user.reload user.reload
expect(user.email_confirmed?).to eq(true) expect(user.email_confirmed?).to eq(true)
expect(user.user_auth_tokens.last.authenticated_with_oauth).to be true
end end
it "should return the authenticated response with the correct path for subfolders" do it "should return the authenticated response with the correct path for subfolders" do

View File

@ -2010,6 +2010,7 @@ RSpec.describe SessionController do
expect(session[:current_user_id]).to eq(user.id) expect(session[:current_user_id]).to eq(user.id)
expect(user.user_auth_tokens.count).to eq(1) expect(user.user_auth_tokens.count).to eq(1)
expect(user.user_auth_tokens.last.authenticated_with_oauth).to be false
unhashed_token = decrypt_auth_cookie(cookies[:_t])[:token] unhashed_token = decrypt_auth_cookie(cookies[:_t])[:token]
expect(UserAuthToken.hash_token(unhashed_token)).to eq( expect(UserAuthToken.hash_token(unhashed_token)).to eq(
user.user_auth_tokens.first.auth_token, user.user_auth_tokens.first.auth_token,