v0.10.1 -- restored optimal design, resolved multiple issues, notable progress
This commit is contained in:
parent
ccc85e3856
commit
4472e9b0e3
307
plugin.rb
307
plugin.rb
@ -4,32 +4,132 @@
|
|||||||
|
|
||||||
# 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.9.6
|
# version: 0.10.1
|
||||||
# authors: saint
|
# authors: saint
|
||||||
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
|
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
|
||||||
|
|
||||||
# require 'digest'
|
# require 'digest'
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
class ::SessionController < ApplicationController
|
# Define a module to contain the MD5 authentication logic
|
||||||
|
module LegacyMd5Authentication
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
def to64(value, length)
|
||||||
|
result = String.new
|
||||||
|
length.times do
|
||||||
|
result << ITOA64[value & 0x3f]
|
||||||
|
value >>= 6
|
||||||
|
Rails.logger.debug "to64 result: #{result}"
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def gossamer_md5_crypt(password, legacy_hash)
|
||||||
|
# Extract the salt from the legacy hash
|
||||||
|
parts = legacy_hash.split('$')
|
||||||
|
salt = parts[2]
|
||||||
|
|
||||||
|
# Limit the salt to 8 characters
|
||||||
|
salt = salt[0, 8]
|
||||||
|
|
||||||
|
magic = "$GT$"
|
||||||
|
Rails.logger.debug "MD5 magic: #{magic}"
|
||||||
|
|
||||||
|
ctx = Digest::MD5.new
|
||||||
|
ctx.update(password)
|
||||||
|
ctx.update(magic)
|
||||||
|
ctx.update(salt)
|
||||||
|
|
||||||
|
final = Digest::MD5.new
|
||||||
|
final.update(password)
|
||||||
|
final.update(salt)
|
||||||
|
final.update(password)
|
||||||
|
final_digest = final.digest
|
||||||
|
|
||||||
|
password_length = password.length
|
||||||
|
|
||||||
|
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.debug "MD5 final_digest: #{final_digest}"
|
||||||
|
|
||||||
|
1000.times do |i|
|
||||||
|
ctx1 = Digest::MD5.new
|
||||||
|
if i & 1 != 0
|
||||||
|
Rails.logger.debug "AAA"
|
||||||
|
ctx1.update(password)
|
||||||
|
else
|
||||||
|
Rails.logger.debug "BBB"
|
||||||
|
ctx1.update(final_digest)
|
||||||
|
end
|
||||||
|
ctx1.update(salt) if i % 3 != 0
|
||||||
|
ctx1.update(password) if i % 7 != 0
|
||||||
|
if i & 1 != 0
|
||||||
|
Rails.logger.debug "CCC"
|
||||||
|
ctx1.update(final_digest)
|
||||||
|
else
|
||||||
|
Rails.logger.debug "DDD"
|
||||||
|
ctx1.update(password)
|
||||||
|
end
|
||||||
|
final_digest = ctx1.digest
|
||||||
|
end
|
||||||
|
Rails.logger.debug "MD6 final_digest: #{final_digest}"
|
||||||
|
|
||||||
|
result = String.new
|
||||||
|
Rails.logger.debug "A result: #{result}"
|
||||||
|
result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4)
|
||||||
|
Rails.logger.debug "B result: #{result}"
|
||||||
|
result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4)
|
||||||
|
Rails.logger.debug "C result: #{result}"
|
||||||
|
result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4)
|
||||||
|
Rails.logger.debug "D result: #{result}"
|
||||||
|
result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4)
|
||||||
|
Rails.logger.debug "E result: #{result}"
|
||||||
|
result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4)
|
||||||
|
Rails.logger.debug "F result: #{result}"
|
||||||
|
result << to64(final_digest[11].ord, 2)
|
||||||
|
Rails.logger.debug "G result: #{result}"
|
||||||
|
|
||||||
|
Rails.logger.debug "magic salt result #{magic}#{salt}$#{result}"
|
||||||
|
"#{magic}#{salt}$#{result}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_gossamer_password(password, legacy_hash)
|
||||||
|
generated_hash = gossamer_md5_crypt(password, legacy_hash)
|
||||||
|
generated_hash == legacy_hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extend the SessionController create method to include our MD5 authentication logic
|
||||||
|
class ::SessionController < ApplicationController
|
||||||
|
prepend LegacyMd5Authentication
|
||||||
|
|
||||||
def create
|
def create
|
||||||
# Require the necessary parameters
|
|
||||||
params.require(:login)
|
params.require(:login)
|
||||||
params.require(:password)
|
params.require(:password)
|
||||||
|
|
||||||
# Validate password length
|
|
||||||
return invalid_credentials if params[:password].length > User.max_password_length
|
return invalid_credentials if params[:password].length > User.max_password_length
|
||||||
|
|
||||||
# Find user by username or email
|
|
||||||
user = User.find_by_username_or_email(normalized_login_param)
|
user = User.find_by_username_or_email(normalized_login_param)
|
||||||
|
|
||||||
# Check for read-only mode for non-staff users
|
|
||||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
|
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
|
||||||
|
|
||||||
# Apply rate limit for second factor authentication
|
|
||||||
rate_limit_second_factor!(user)
|
rate_limit_second_factor!(user)
|
||||||
|
|
||||||
if user.present?
|
if user.present?
|
||||||
@ -37,162 +137,107 @@ after_initialize do
|
|||||||
custom_password_md5 = user.custom_fields['custom_password_md5']
|
custom_password_md5 = user.custom_fields['custom_password_md5']
|
||||||
|
|
||||||
# Check for MD5 password in custom field
|
# Check for MD5 password in custom field
|
||||||
|
Rails.logger.debug "Check for MD5 password in custom field"
|
||||||
if custom_password_md5.present?
|
if custom_password_md5.present?
|
||||||
Rails.logger.debug "MD5 password is present. custom_password_md5: #{custom_password_md5}, password: #{password}"
|
# MD5 password is present
|
||||||
|
Rails.logger.debug "MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}"
|
||||||
|
|
||||||
# Extract the salt from the legacy hash
|
if verify_gossamer_password(password, custom_password_md5)
|
||||||
parts = custom_password_md5.split('$')
|
# MD5 matches, so update the user's password to the new one, remove the custom field and ensure user is set to active and approved
|
||||||
Rails.logger.debug "Split parts: #{parts.inspect}"
|
Rails.logger.debug "MD5 matches"
|
||||||
|
|
||||||
if parts.length >= 3
|
# Set password using Discourse's current standards, ensuring correct hashing.
|
||||||
salt = parts[2][0, 8]
|
|
||||||
else
|
|
||||||
Rails.logger.debug "Invalid MD5 format for custom_password_md5: #{custom_password_md5}"
|
|
||||||
return invalid_credentials
|
|
||||||
end
|
|
||||||
|
|
||||||
magic = "$GT$"
|
|
||||||
Rails.logger.debug "MD5 magic: #{magic}, salt: #{salt}"
|
|
||||||
|
|
||||||
# Create initial MD5 context
|
|
||||||
ctx = Digest::MD5.new
|
|
||||||
ctx.update(password)
|
|
||||||
ctx.update(magic)
|
|
||||||
ctx.update(salt)
|
|
||||||
|
|
||||||
# Create final MD5 digest
|
|
||||||
final = Digest::MD5.new
|
|
||||||
final.update(password)
|
|
||||||
final.update(salt)
|
|
||||||
final.update(password)
|
|
||||||
final_digest = final.digest.dup # Ensure final_digest is not frozen
|
|
||||||
|
|
||||||
# Perform password length operations
|
|
||||||
password_length = password.length
|
|
||||||
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.dup # Ensure final_digest is not frozen
|
|
||||||
Rails.logger.debug "MD5 final_digest after initial operations: #{final_digest}"
|
|
||||||
|
|
||||||
# Perform 1000 iterations of MD5 hashing
|
|
||||||
1000.times do |i|
|
|
||||||
ctx1 = Digest::MD5.new
|
|
||||||
if i & 1 != 0
|
|
||||||
ctx1.update(password)
|
|
||||||
else
|
|
||||||
ctx1.update(final_digest.dup) # Ensure final_digest is not frozen
|
|
||||||
end
|
|
||||||
ctx1.update(salt) if i % 3 != 0
|
|
||||||
ctx1.update(password) if i % 7 != 0
|
|
||||||
if i & 1 != 0
|
|
||||||
ctx1.update(final_digest.dup) # Ensure final_digest is not frozen
|
|
||||||
else
|
|
||||||
ctx1.update(password)
|
|
||||||
end
|
|
||||||
final_digest = ctx1.digest.dup # Ensure final_digest is not frozen
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convert final digest to the hashed password format
|
|
||||||
result = ''
|
|
||||||
length = 4
|
|
||||||
value = (final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
value = (final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
value = (final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
value = (final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
value = (final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
length = 2
|
|
||||||
value = final_digest[11].ord
|
|
||||||
length.times do
|
|
||||||
result << ITOA64[value & 0x3f]
|
|
||||||
value >>= 6
|
|
||||||
end
|
|
||||||
|
|
||||||
entered_password_md5 = "#{magic}#{salt}$#{result}"
|
|
||||||
Rails.logger.debug "Generated MD5 hash: #{entered_password_md5}"
|
|
||||||
|
|
||||||
# Verify the entered password MD5 hash against the stored hash
|
|
||||||
if entered_password_md5 == custom_password_md5
|
|
||||||
Rails.logger.debug "MD5 hash matches for user: #{user.id}"
|
|
||||||
|
|
||||||
# MD5 matches, update the user's password to the new one and remove the custom field
|
|
||||||
user.password = password
|
user.password = password
|
||||||
user.custom_fields['custom_password_md5'] = nil
|
|
||||||
|
# Set other attributes
|
||||||
|
user.active = true
|
||||||
|
user.approved = true
|
||||||
|
# user.email_confirmed = true
|
||||||
|
user.approved_at = Time.now
|
||||||
|
user.approved_by_id = 1
|
||||||
|
|
||||||
|
#
|
||||||
|
# hashed_password = UserAuthenticator.new(nil).password_digest(password)
|
||||||
|
# user.update_columns(
|
||||||
|
# hashed_password: hashed_password
|
||||||
|
# )
|
||||||
|
|
||||||
|
# user.custom_fields['custom_password_md5'] = nil
|
||||||
|
|
||||||
|
# user.active = true
|
||||||
|
# user.approved = true
|
||||||
|
|
||||||
|
# # Initialize UserAuthenticator with user and session
|
||||||
|
# authenticator = UserAuthenticator.new(user, session)
|
||||||
|
|
||||||
|
# # Generate a salted password hash for the new password
|
||||||
|
# hashed_password = authenticator.password_digest(password)
|
||||||
|
# Rails.logger.debug "NEW hashed_password #{hashed_password}"
|
||||||
|
|
||||||
|
# # Update the user object with all changes
|
||||||
|
# user.assign_attributes(
|
||||||
|
# password_hash: hashed_password,
|
||||||
|
# # salt: authenticator.salt,
|
||||||
|
# # password_algorithm: authenticator.algorithm_name,
|
||||||
|
# active: true,
|
||||||
|
# approved: true,
|
||||||
|
# custom_fields: { 'custom_password_md5' => nil }
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Save the changes
|
||||||
user.save!
|
user.save!
|
||||||
Rails.logger.debug "Updated MD5 password for user: #{user.id}"
|
|
||||||
|
# if user.save
|
||||||
|
# Rails.logger.debug "User changes saved: #{user.username}"
|
||||||
|
# else
|
||||||
|
# Rails.logger.debug "User changes FAILED: #{user.errors.full_messages}"
|
||||||
|
# invalid_credentials
|
||||||
|
# return
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
Rails.logger.debug "Updated user: #{user.id}"
|
||||||
else
|
else
|
||||||
Rails.logger.debug "MD5 password incorrect for user: #{user.id}"
|
# MD5 doesn't match, so we have a failed login attempt.
|
||||||
return invalid_credentials
|
Rails.logger.debug "MD5 Password incorrect for user: #{user.id}"
|
||||||
|
invalid_credentials
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
elsif !user.confirm_password?(password)
|
elsif !user.confirm_password?(password)
|
||||||
|
# There is no MD5 password and the password was incorrect.
|
||||||
Rails.logger.debug "Password incorrect for user: #{user.id}"
|
Rails.logger.debug "Password incorrect for user: #{user.id}"
|
||||||
return invalid_credentials
|
invalid_credentials
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle user approval requirements
|
# If the site requires user approval and the user is not approved yet
|
||||||
if login_not_approved_for?(user)
|
if login_not_approved_for?(user)
|
||||||
render json: login_not_approved
|
render json: login_not_approved
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Invalidate invite link if user signed on with username and password
|
# User signed on with username and password, so let's prevent the invite link
|
||||||
|
# from being used to log in (if one exists).
|
||||||
Invite.invalidate_for_email(user.email)
|
Invite.invalidate_for_email(user.email)
|
||||||
|
|
||||||
# Handle password expiration
|
# User's password has expired so they need to reset it
|
||||||
if user.password_expired?(password)
|
if user.password_expired?(password)
|
||||||
render json: { error: "expired", reason: "expired" }
|
render json: { error: "expired", reason: "expired" }
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Rails.logger.debug "User not found with login: #{params[:login]}"
|
invalid_credentials
|
||||||
return invalid_credentials
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if payload = login_error_check(user)
|
if payload = login_error_check(user)
|
||||||
return render json: payload
|
return render json: payload
|
||||||
end
|
end
|
||||||
|
|
||||||
# Perform second factor authentication
|
|
||||||
second_factor_auth_result = authenticate_second_factor(user)
|
second_factor_auth_result = authenticate_second_factor(user)
|
||||||
unless second_factor_auth_result.ok
|
return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok
|
||||||
Rails.logger.debug "Second factor authentication failed for user: #{user.id}"
|
|
||||||
return render(json: @second_factor_failure_payload)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle active and email confirmed users
|
|
||||||
if user.active && user.email_confirmed?
|
if user.active && user.email_confirmed?
|
||||||
login(user, second_factor_auth_result)
|
login(user, second_factor_auth_result)
|
||||||
else
|
else
|
||||||
|
Loading…
Reference in New Issue
Block a user