v0.16 Further fixups to work with Bitnami Discourse 3.2.5

This commit is contained in:
David Sainty 2024-08-28 21:48:07 +10:00
parent 9edabc2a3a
commit 7226bf0b9a
2 changed files with 173 additions and 165 deletions

BIN
.plugin.rb.bck.swp Normal file

Binary file not shown.

338
plugin.rb
View File

@ -4,7 +4,7 @@
# name: discourse-md5_authentication # name: discourse-md5_authentication
# about: A plugin to authenticate users with MD5 passwords from legacy systems # about: A plugin to authenticate users with MD5 passwords from legacy systems
# version: 0.14 # version: 0.16
# authors: saint@federated.computer # authors: saint@federated.computer
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git # url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
@ -13,200 +13,208 @@
# enabled_site_setting :legacymd5password_auth_enabled # enabled_site_setting :legacymd5password_auth_enabled
after_initialize do after_initialize do
# Extend the SessionController class to include our custom authentication logic # Define a module to hold the legacy MD5 authentication logic
class ::SessionController module LegacyMd5Authentication
prepend Module.new {
# Override the create method to add our custom authentication checks
def create
Rails.logger.warn "MD5 -- AA -- start create"
# Ensure required parameters are present
params.require(:login)
params.require(:password)
# Validate the length of the password # Constants
return invalid_credentials if params[:password].length > User.max_password_length ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
# Find the user by username or email # Override the create method to add our custom authentication checks
user = User.find_by_username_or_email(normalized_login_param) def create
Rails.logger.warn "MD5 -- AA -- start create"
# Ensure required parameters are present
params.require(:login)
params.require(:password)
Rails.logger.warn "MD5 -- BB -- second" # Validate the length of the password
# Check if site is in staff writes-only mode and ensure user is staff if true return invalid_credentials if params[:password].length > User.max_password_length
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
# Apply rate limiting for second factor authentication # Find the user by username or email
rate_limit_second_factor!(user) user = User.find_by_username_or_email(normalized_login_param)
if user.present? Rails.logger.warn "MD5 -- BB -- second"
Rails.logger.warn "MD5 -- CC -- user.present" # Check if site is in staff writes-only mode and ensure user is staff if true
# Retrieve the provided password and custom MD5 password hash from user custom fields raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
password = params[:password]
custom_password_md5 = user.custom_fields['custom_password_md5']
# Log the presence of custom MD5 hash for debugging # Apply rate limiting for second factor authentication
Rails.logger.warn "MD5 -- Check for MD5 password in custom field" rate_limit_second_factor!(user)
if custom_password_md5.present?
Rails.logger.warn "MD5 -- MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}"
# Verify the provided password against the stored MD5 hash if user.present?
if verify_gossamer_password(password, custom_password_md5) Rails.logger.warn "MD5 -- CC -- user.present"
# If MD5 hash matches, update the user's password and other attributes # Retrieve the provided password and custom MD5 password hash from user custom fields
Rails.logger.warn "MD5 -- MD5 matches" password = params[:password]
custom_password_md5 = user.custom_fields['custom_password_md5']
# Set the user's password to the provided one and update other attributes # Log the presence of custom MD5 hash for debugging
user.password = password Rails.logger.warn "MD5 -- Check for MD5 password in custom field"
user.active = true if custom_password_md5.present?
user.approved = true Rails.logger.warn "MD5 -- MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}"
user.approved_at = Time.now
user.approved_by_id = 1 # Verify the provided password against the stored MD5 hash
user.custom_fields['custom_password_md5'] = nil # Clear the custom MD5 field if verify_gossamer_password(password, custom_password_md5)
user.save! # If MD5 hash matches, update the user's password and other attributes
Rails.logger.warn "MD5 -- MD5 matches"
# Set the user's password to the provided one and update other attributes
user.password = password
user.active = true
user.approved = true
user.approved_at = Time.now
user.approved_by_id = 1
user.custom_fields['custom_password_md5'] = nil # Clear the custom MD5 field
user.save!
Rails.logger.warn "MD5 -- DD -- user.present" Rails.logger.warn "MD5 -- DD -- user.present"
# Generate a new token and hash it # Generate a new token and hash it
token = SecureRandom.hex(20) token = SecureRandom.hex(20)
token_hash = EmailToken.hash_token(token) token_hash = EmailToken.hash_token(token)
# Create a confirmed email token for the user # Create a confirmed email token for the user
EmailToken.create!( EmailToken.create!(
user_id: user.id, user_id: user.id,
email: user.email, email: user.email,
token_hash: token_hash, token_hash: token_hash,
confirmed: true confirmed: true
) )
Rails.logger.warn("MD5 -- Generated token for user #{user.username}: #{token}") Rails.logger.warn("MD5 -- Generated token for user #{user.username}: #{token}")
Rails.logger.warn "MD5 -- Updated user: #{user.id}" Rails.logger.warn "MD5 -- Updated user: #{user.id}"
else else
# If MD5 hash does not match, log the failed login attempt # If MD5 hash does not match, log the failed login attempt
Rails.logger.warn "MD5 -- MD5 Password (hash) incorrect for user: #{user.id}" Rails.logger.warn "MD5 -- MD5 Password (hash) incorrect for user: #{user.id}"
invalid_credentials
return
end
elsif !user.confirm_password?(password)
# If no MD5 hash is present and the provided password is incorrect
Rails.logger.warn "MD5 -- Password incorrect for user: #{user.id}"
invalid_credentials invalid_credentials
return return
end end
# If the site requires user approval and the user is not yet approved elsif !user.confirm_password?(password)
if login_not_approved_for?(user) # If no MD5 hash is present and the provided password is incorrect
render json: login_not_approved Rails.logger.warn "MD5 -- Password incorrect for user: #{user.id}"
return
end
# Invalidate any invite link for the user
Invite.invalidate_for_email(user.email)
# Check if the user's password has expired
if user.password_expired?(password)
render json: { error: "expired", reason: "expired" }
return
end
else
# If no user is found with the provided credentials
invalid_credentials invalid_credentials
return return
end end
# Check for any login errors # If the site requires user approval and the user is not yet approved
if payload = login_error_check(user) if login_not_approved_for?(user)
return render json: payload render json: login_not_approved
return
end end
# Authenticate second factor if required # Invalidate any invite link for the user
second_factor_auth_result = authenticate_second_factor(user) Invite.invalidate_for_email(user.email)
return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok
# If user is active and email is confirmed, proceed with login # Check if the user's password has expired
if user.active && user.email_confirmed? if user.password_expired?(password)
login(user, second_factor_auth_result) render json: { error: "expired", reason: "expired" }
else return
not_activated(user)
end end
else
# If no user is found with the provided credentials
invalid_credentials
return
end end
}
end
end
# Helper methods to handle MD5 password verification # Check for any login errors
if payload = login_error_check(user)
return render json: payload
end
def to64(value, length) # Authenticate second factor if required
# Convert a value to a base64-like representation second_factor_auth_result = authenticate_second_factor(user)
result = String.new return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok
length.times do
result << ITOA64[value & 0x3f]
value >>= 6
Rails.logger.warn "to64 result: #{result}"
end
result
end
def gossamer_md5_crypt(password, legacy_hash) # If user is active and email is confirmed, proceed with login
# Extract the salt from the legacy hash if user.active && user.email_confirmed?
parts = legacy_hash.split('$') login(user, second_factor_auth_result)
salt = parts[2] else
not_activated(user)
# Limit the salt to 8 characters end
salt = salt[0, 8] end
magic = "$GT$" # Helper methods to handle MD5 password verification
Rails.logger.warn "MD5 magic: #{magic}"
def to64(value, length)
ctx = Digest::MD5.new # Convert a value to a base64-like representation
ctx.update(password) result = String.new
ctx.update(magic) length.times do
ctx.update(salt) result << ITOA64[value & 0x3f]
value >>= 6
final = Digest::MD5.new Rails.logger.warn "to64 result: #{result}"
final.update(password) end
final.update(salt) result
final.update(password) end
final_digest = final.digest
def gossamer_md5_crypt(password, legacy_hash)
password_length = password.length # Extract the salt from the legacy hash
parts = legacy_hash.split('$')
# Process the password with various hashing operations salt = parts[2]
while password_length > 0
ctx.update(final_digest[0, [password_length, 16].min]) # Limit the salt to 8 characters
password_length -= 16 salt = salt[0, 8]
end
magic = "$GT$"
password_length = password.length Rails.logger.warn "MD5 magic: #{magic}"
while password_length > 0
if password_length & 1 != 0 ctx = Digest::MD5.new
ctx.update("\x00") ctx.update(password)
else ctx.update(magic)
ctx.update(password[0, 1]) ctx.update(salt)
final = Digest::MD5.new
final.update(password)
final.update(salt)
final.update(password)
final_digest = final.digest
password_length = password.length
# Process the password with various hashing operations
while password_length > 0
ctx.update(final_digest[0, [password_length, 16].min])
password_length -= 16
end
password_length = password.length
while password_length > 0
if password_length & 1 != 0
ctx.update("\x00")
else
ctx.update(password[0, 1])
end
password_length >>= 1
end
final_digest = ctx.digest
Rails.logger.warn "MD6 final_digest: #{final_digest}"
result = String.new
Rails.logger.warn "A result: #{result}"
result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4)
Rails.logger.warn "B result: #{result}"
result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4)
Rails.logger.warn "C result: #{result}"
result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4)
Rails.logger.warn "D result: #{result}"
result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4)
Rails.logger.warn "E result: #{result}"
result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4)
Rails.logger.warn "F result: #{result}"
result << to64(final_digest[11].ord, 2)
Rails.logger.warn "G result: #{result}"
Rails.logger.warn "magic salt result #{magic}#{salt}$#{result}"
"#{magic}#{salt}$#{result}"
end
def verify_gossamer_password(password, legacy_hash)
# Verify the provided password against the stored MD5 hash
generated_hash = gossamer_md5_crypt(password, legacy_hash)
generated_hash == legacy_hash
end end
password_length >>= 1
end end
final_digest = ctx.digest # Extend the SessionController class to include the LegacyMd5Authentication module
Rails.logger.warn "MD6 final_digest: #{final_digest}" class ::SessionController
prepend LegacyMd5Authentication
result = String.new end
Rails.logger.warn "A result: #{result}"
result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4)
Rails.logger.warn "B result: #{result}"
result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4)
Rails.logger.warn "C result: #{result}"
result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4)
Rails.logger.warn "D result: #{result}"
result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4)
Rails.logger.warn "E result: #{result}"
result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4)
Rails.logger.warn "F result: #{result}"
result << to64(final_digest[11].ord, 2)
Rails.logger.warn "G result: #{result}"
Rails.logger.warn "magic salt result #{magic}#{salt}$#{result}"
"#{magic}#{salt}$#{result}"
end end
def verify_gossamer_password(password, legacy_hash)
# Verify the provided password against the stored MD5 hash
generated_hash = gossamer_md5_crypt(password, legacy_hash)
generated_hash == legacy_hash
end