2024-06-11 05:28:46 +00:00
# plugins/discourse-md5_authentication/plugin.rb
2024-06-11 04:26:59 +00:00
# frozen_string_literal: true
2024-06-11 05:28:46 +00:00
# name: discourse-md5_authentication
2024-06-11 04:26:59 +00:00
# about: A plugin to authenticate users with MD5 passwords from legacy systems
2024-06-17 08:44:22 +00:00
# version: 0.12.1
2024-06-11 04:26:59 +00:00
# authors: saint
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
2024-06-17 08:38:05 +00:00
# require 'digest'
2024-06-11 04:26:59 +00:00
after_initialize do
2024-06-17 08:43:07 +00:00
# Define a module to contain the MD5 authentication logic
module LegacyMd5Authentication
2024-06-17 08:40:28 +00:00
# Constants
2024-06-17 08:38:05 +00:00
ITOA64 = " ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
2024-06-17 08:43:07 +00:00
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
2024-06-11 13:27:00 +00:00
def create
params . require ( :login )
params . require ( :password )
return invalid_credentials if params [ :password ] . length > User . max_password_length
user = User . find_by_username_or_email ( normalized_login_param )
raise Discourse :: ReadOnly if staff_writes_only_mode? && ! user & . staff?
rate_limit_second_factor! ( user )
if user . present?
password = params [ :password ]
custom_password_md5 = user . custom_fields [ 'custom_password_md5' ]
# Check for MD5 password in custom field
2024-06-17 08:43:07 +00:00
Rails . logger . debug " Check for MD5 password in custom field "
2024-06-11 13:27:00 +00:00
if custom_password_md5 . present?
2024-06-17 08:43:07 +00:00
# MD5 password is present
Rails . logger . debug " MD5 password is present custom_password_md5: #{ custom_password_md5 } password: #{ password } "
2024-06-17 08:40:28 +00:00
2024-06-17 08:43:07 +00:00
if verify_gossamer_password ( password , custom_password_md5 )
# 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 " MD5 matches "
2024-06-17 08:40:28 +00:00
2024-06-17 08:44:22 +00:00
# Set password using Discourse's current standards, ensuring correct hashing, with exception check for the same password as that alaedy stored in Discourse
2024-06-17 08:43:07 +00:00
user . password = password
2024-06-17 08:40:28 +00:00
2024-06-17 08:43:07 +00:00
# Set other attributes
user . active = true
user . approved = true
# user.email_confirmed = true
user . approved_at = Time . now
user . approved_by_id = 1
2024-06-17 08:44:22 +00:00
user . custom_fields [ 'custom_password_md5' ] = nil
user . save!
2024-06-17 08:43:07 +00:00
2024-06-17 08:44:22 +00:00
# Generate a new token and hash it
token = SecureRandom . hex ( 20 )
token_hash = EmailToken . hash_token ( token )
# Create a confirmed e-mail token
EmailToken . create! (
user_id : user . id ,
email : user . email ,
token_hash : token_hash ,
confirmed : true
)
Rails . logger . debug ( " Generated token for user #{ user . username } : #{ token } " )
2024-06-17 08:43:07 +00:00
# # 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 }
# )
2024-06-17 08:40:28 +00:00
2024-06-17 08:43:07 +00:00
# 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
2024-06-17 08:40:28 +00:00
2024-06-11 13:27:00 +00:00
2024-06-17 08:43:07 +00:00
Rails . logger . debug " Updated user: #{ user . id } "
2024-06-11 13:27:00 +00:00
else
2024-06-17 08:43:07 +00:00
# MD5 doesn't match, so we have a failed login attempt.
Rails . logger . debug " MD5 Password incorrect for user: #{ user . id } "
invalid_credentials
return
2024-06-11 13:27:00 +00:00
end
elsif ! user . confirm_password? ( password )
2024-06-17 08:43:07 +00:00
# There is no MD5 password and the password was incorrect.
2024-06-11 13:27:00 +00:00
Rails . logger . debug " Password incorrect for user: #{ user . id } "
2024-06-17 08:43:07 +00:00
invalid_credentials
return
2024-06-11 09:16:11 +00:00
end
2024-06-17 08:43:07 +00:00
# If the site requires user approval and the user is not approved yet
2024-06-11 13:27:00 +00:00
if login_not_approved_for? ( user )
render json : login_not_approved
return
end
2024-06-17 08:43:07 +00:00
# User signed on with username and password, so let's prevent the invite link
# from being used to log in (if one exists).
2024-06-11 13:27:00 +00:00
Invite . invalidate_for_email ( user . email )
2024-06-17 08:43:07 +00:00
# User's password has expired so they need to reset it
2024-06-11 13:27:00 +00:00
if user . password_expired? ( password )
render json : { error : " expired " , reason : " expired " }
return
end
2024-06-11 09:16:11 +00:00
else
2024-06-17 08:43:07 +00:00
invalid_credentials
return
2024-06-11 09:16:11 +00:00
end
2024-06-11 13:27:00 +00:00
if payload = login_error_check ( user )
return render json : payload
2024-06-11 04:26:59 +00:00
end
2024-06-11 13:27:00 +00:00
second_factor_auth_result = authenticate_second_factor ( user )
2024-06-17 08:43:07 +00:00
return render ( json : @second_factor_failure_payload ) unless second_factor_auth_result . ok
2024-06-11 13:27:00 +00:00
if user . active && user . email_confirmed?
login ( user , second_factor_auth_result )
else
not_activated ( user )
end
2024-06-11 08:45:16 +00:00
end
2024-06-11 04:26:59 +00:00
end
end
2024-06-17 08:38:05 +00:00