@@ -23,18 +23,35 @@ module Auth
2323
2424 ##
2525 # Authenticate a request and return account data if valid
26- # @param request [Roda ::Request] request object
26+ # @param request [Rack ::Request] request object
2727 # @return [Hash, nil] account data if authenticated
2828 def authenticate ( request )
2929 token = extract_token ( request )
30- return nil unless token
30+ return log_auth_failure ( request , 'missing_token' ) unless token
3131
3232 account = get_account ( token )
33- if account
34- SecurityLogger . log_auth_failure ( request . ip , request . user_agent , 'success' )
35- else
36- SecurityLogger . log_auth_failure ( request . ip , request . user_agent , 'invalid_token' )
37- end
33+ return log_auth_success ( account , request ) if account
34+
35+ log_auth_failure ( request , 'invalid_token' )
36+ end
37+
38+ ##
39+ # Log auth failure and return nil
40+ # @param request [Rack::Request] request object
41+ # @param reason [String] failure reason
42+ # @return [nil]
43+ def log_auth_failure ( request , reason )
44+ SecurityLogger . log_auth_failure ( request . ip , request . user_agent , reason )
45+ nil
46+ end
47+
48+ ##
49+ # Log auth success and return account
50+ # @param account [Hash] account data
51+ # @param request [Rack::Request] request object
52+ # @return [Hash] account data
53+ def log_auth_success ( account , request )
54+ SecurityLogger . log_auth_success ( account [ :username ] , request . ip )
3855 account
3956 end
4057
@@ -43,7 +60,7 @@ def authenticate(request)
4360 # @param token [String] authentication token
4461 # @return [Hash, nil] account data if found
4562 def get_account ( token )
46- return nil unless token
63+ return nil unless token && token_index . key? ( token )
4764
4865 token_index [ token ]
4966 end
@@ -52,7 +69,14 @@ def get_account(token)
5269 # Get token index for O(1) lookups
5370 # @return [Hash] token to account mapping
5471 def token_index
55- @token_index ||= accounts . each_with_object ( { } ) { |account , hash | hash [ account [ :token ] ] = account } # rubocop:disable ThreadSafety/ClassInstanceVariable
72+ @token_index ||= build_token_index # rubocop:disable ThreadSafety/ClassInstanceVariable
73+ end
74+
75+ ##
76+ # Build token index in a thread-safe manner
77+ # @return [Hash] token to account mapping
78+ def build_token_index
79+ accounts . each_with_object ( { } ) { |account , hash | hash [ account [ :token ] ] = account }
5680 end
5781
5882 ##
@@ -127,7 +151,7 @@ def validate_feed_token(feed_token, url)
127151 return nil unless valid
128152
129153 get_account_by_username ( token_data [ :payload ] [ :username ] )
130- rescue StandardError
154+ rescue JSON :: ParserError , ArgumentError
131155 SecurityLogger . log_token_usage ( feed_token , url , false )
132156 nil
133157 end
@@ -174,7 +198,7 @@ def token_valid?(token_data, url)
174198 # @param url [String] full URL with query parameters
175199 # @return [String, nil] feed token if found
176200 def extract_feed_token_from_url ( url )
177- URI . parse ( url ) . then { |uri | URI . decode_www_form ( uri . query || '' ) . to_h [ 'token' ] }
201+ URI . parse ( url ) . then { |uri | CGI . parse ( uri . query || '' ) [ 'token' ] &. first }
178202 rescue StandardError
179203 nil
180204 end
@@ -193,7 +217,7 @@ def feed_url_allowed?(feed_token, url)
193217
194218 ##
195219 # Extract token from request (Authorization header only)
196- # @param request [Roda ::Request] request object
220+ # @param request [Rack ::Request] request object
197221 # @return [String, nil] token if found
198222 def extract_token ( request )
199223 auth_header = request . env [ 'HTTP_AUTHORIZATION' ]
@@ -266,7 +290,8 @@ def url_matches_pattern?(url, pattern)
266290 escaped_pattern = Regexp . escape ( pattern ) . gsub ( '\\*' , '.*' )
267291 url . match? ( /\A #{ escaped_pattern } \z / )
268292 else
269- url . include? ( pattern )
293+ # Exact match for non-wildcard patterns
294+ url == pattern
270295 end
271296 end
272297
@@ -282,38 +307,18 @@ def sanitize_xml(text)
282307 end
283308
284309 ##
285- # Validate URL format and scheme using Html2rss::Url.for_channel
310+ # Validate URL format and scheme
286311 # @param url [String] URL to validate
287- # @return [Boolean] true if URL is valid and allowed
312+ # @return [Boolean] true if URL is valid
288313 def valid_url? ( url )
289- return false unless basic_url_valid? ( url )
314+ return false unless url . is_a? ( String ) && ! url . empty? && url . length <= 2048
290315
291- validate_url_with_html2rss ( url )
316+ uri = URI . parse ( url )
317+ uri . is_a? ( URI ::HTTP ) || uri . is_a? ( URI ::HTTPS )
292318 rescue StandardError
293319 false
294320 end
295321
296- ##
297- # Basic URL format validation
298- # @param url [String] URL to validate
299- # @return [Boolean] true if basic format is valid
300- def basic_url_valid? ( url )
301- url . is_a? ( String ) && !url . empty? && url . length <= 2048 && url . match? ( %r{\A https?://.+} )
302- end
303-
304- ##
305- # Validate URL using Html2rss if available, otherwise basic validation
306- # @param url [String] URL to validate
307- # @return [Boolean] true if URL is valid
308- def validate_url_with_html2rss ( url )
309- if defined? ( Html2rss ::Url ) && Html2rss ::Url . respond_to? ( :for_channel )
310- !Html2rss ::Url . for_channel ( url ) . nil?
311- else
312- # Fallback to basic URL validation for tests
313- URI . parse ( url ) . is_a? ( URI ::HTTP ) || URI . parse ( url ) . is_a? ( URI ::HTTPS )
314- end
315- end
316-
317322 ##
318323 # Validate username format and length
319324 # @param username [String] username to validate
0 commit comments