2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2009-07-27 15:04:54 +00:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
2017-05-27 17:28:07 +02:00
# Copyright © 2008-2017 Guillaume Ayoub
2019-06-17 04:13:25 +02:00
# Copyright © 2017-2019 Unrud <unrud@outlook.com>
2025-01-01 15:47:22 +01:00
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
2008-12-30 16:25:42 +00:00
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
2020-01-12 23:32:28 +01:00
"""
Authentication backend that checks credentials with a htpasswd file .
Apache ' s htpasswd command (httpd.apache.org/docs/programs/htpasswd.html)
manages a file for storing user credentials . It can encrypt passwords using
2024-03-12 06:08:26 +01:00
different the methods BCRYPT / SHA256 / SHA512 or MD5 - APR1 ( a version of MD5 modified for
Apache ) . MD5 - APR1 provides medium security as of 2015. Only BCRYPT / SHA256 / SHA512 can be
2020-01-19 18:26:14 +01:00
considered secure by current standards .
2020-01-12 23:32:28 +01:00
MD5 - APR1 - encrypted credentials can be written by all versions of htpasswd ( it
2024-03-12 06:08:26 +01:00
is the default , in fact ) , whereas BCRYPT / SHA256 / SHA512 requires htpasswd 2.4 . x or newer .
2020-01-12 23:32:28 +01:00
The ` is_authenticated ( user , password ) ` function provided by this module
verifies the user - given credentials by parsing the htpasswd credential file
pointed to by the ` ` htpasswd_filename ` ` configuration value while assuming
the password encryption method specified via the ` ` htpasswd_encryption ` `
configuration value .
2024-07-24 11:22:49 +02:00
The following htpasswd password encryption methods are supported by Radicale
2020-01-12 23:32:28 +01:00
out - of - the - box :
2024-03-12 06:08:26 +01:00
- plain - text ( created by htpasswd - p . . . ) - - INSECURE
- MD5 - APR1 ( htpasswd - m . . . ) - - htpasswd ' s default method, INSECURE
- SHA256 ( htpasswd - 2 . . . )
- SHA512 ( htpasswd - 5 . . . )
2020-01-12 23:32:28 +01:00
2024-03-07 07:28:43 +01:00
When bcrypt is installed :
2024-03-12 06:08:26 +01:00
- BCRYPT ( htpasswd - B . . . ) - - Requires htpasswd 2.4 . x
2020-01-12 23:32:28 +01:00
2025-04-29 19:14:21 +02:00
When argon2 is installed :
- ARGON2 ( python - c ' from passlib.hash import argon2; print(argon2.using(type= " ID " ).hash( " password " )) '
2020-01-12 23:32:28 +01:00
"""
2016-07-04 14:32:33 +02:00
import functools
2017-05-23 03:02:22 +02:00
import hmac
2024-12-31 18:26:43 +01:00
import os
2025-03-09 08:49:30 +01:00
import re
2024-12-31 16:14:38 +01:00
import threading
2024-12-31 18:26:43 +01:00
import time
from typing import Any , Tuple
2009-07-27 15:04:54 +00:00
2024-03-12 06:08:26 +01:00
from passlib . hash import apr_md5_crypt , sha256_crypt , sha512_crypt
2020-01-19 18:26:14 +01:00
2024-03-12 06:08:26 +01:00
from radicale import auth , config , logger
2018-08-16 07:59:55 +02:00
2017-06-21 09:48:57 +02:00
2018-08-28 16:19:36 +02:00
class Auth ( auth . BaseAuth ) :
2021-07-26 20:56:46 +02:00
_filename : str
_encoding : str
2024-12-31 16:14:38 +01:00
_htpasswd : dict # login -> digest
_htpasswd_mtime_ns : int
2024-12-31 18:26:43 +01:00
_htpasswd_size : int
2024-12-31 16:14:38 +01:00
_htpasswd_ok : bool
2024-12-31 18:26:43 +01:00
_htpasswd_not_ok_time : float
2024-12-31 16:14:38 +01:00
_htpasswd_not_ok_reminder_seconds : int
2024-12-31 17:09:21 +01:00
_htpasswd_bcrypt_use : int
2025-04-29 19:15:19 +02:00
_htpasswd_argon2_use : int
2025-01-01 16:31:31 +01:00
_htpasswd_cache : bool
2024-12-31 17:09:21 +01:00
_has_bcrypt : bool
2025-04-29 19:15:19 +02:00
_has_argon2 : bool
2025-03-08 17:26:28 +01:00
_encryption : str
2024-12-31 16:14:38 +01:00
_lock : threading . Lock
2021-07-26 20:56:46 +02:00
def __init__ ( self , configuration : config . Configuration ) - > None :
2018-08-16 07:59:55 +02:00
super ( ) . __init__ ( configuration )
2020-01-19 18:01:13 +01:00
self . _filename = configuration . get ( " auth " , " htpasswd_filename " )
2025-01-01 17:31:16 +01:00
logger . info ( " auth htpasswd file: %r " , self . _filename )
2021-07-26 20:56:46 +02:00
self . _encoding = configuration . get ( " encoding " , " stock " )
2025-01-01 17:31:16 +01:00
logger . info ( " auth htpasswd file encoding: %r " , self . _encoding )
2025-01-01 16:31:31 +01:00
self . _htpasswd_cache = configuration . get ( " auth " , " htpasswd_cache " )
logger . info ( " auth htpasswd cache: %s " , self . _htpasswd_cache )
2025-03-08 17:26:28 +01:00
self . _encryption : str = configuration . get ( " auth " , " htpasswd_encryption " )
logger . info ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' " , self . _encryption )
2024-03-12 06:08:26 +01:00
2024-12-31 17:09:21 +01:00
self . _has_bcrypt = False
2025-04-29 19:15:19 +02:00
self . _has_argon2 = False
2024-12-31 16:14:38 +01:00
self . _htpasswd_ok = False
self . _htpasswd_not_ok_reminder_seconds = 60 # currently hardcoded
2025-04-29 19:15:19 +02:00
( self . _htpasswd_ok , self . _htpasswd_bcrypt_use , self . _htpasswd_argon2_use , self . _htpasswd , self . _htpasswd_size , self . _htpasswd_mtime_ns ) = self . _read_htpasswd ( True , False )
2024-12-31 16:14:38 +01:00
self . _lock = threading . Lock ( )
2025-03-08 17:26:28 +01:00
if self . _encryption == " plain " :
2020-01-19 18:01:13 +01:00
self . _verify = self . _plain
2025-03-08 17:26:28 +01:00
elif self . _encryption == " md5 " :
2020-01-19 18:26:14 +01:00
self . _verify = self . _md5apr1
2025-03-08 17:26:28 +01:00
elif self . _encryption == " sha256 " :
2024-03-12 07:38:40 +01:00
self . _verify = self . _sha256
2025-03-08 17:26:28 +01:00
elif self . _encryption == " sha512 " :
2024-03-12 07:38:40 +01:00
self . _verify = self . _sha512
2025-04-29 19:18:27 +02:00
if self . _encryption == " bcrypt " or self . _encryption == " autodetect " :
2016-04-22 11:37:02 +09:00
try :
2024-03-06 22:42:37 +01:00
import bcrypt
2017-05-31 11:08:32 +02:00
except ImportError as e :
2025-03-08 17:26:28 +01:00
if ( self . _encryption == " autodetect " ) and ( self . _htpasswd_bcrypt_use == 0 ) :
logger . warning ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' which can require bycrypt module, but currently no entries found " , self . _encryption )
2024-12-31 17:09:21 +01:00
else :
raise RuntimeError (
" The htpasswd encryption method ' bcrypt ' or ' autodetect ' requires "
" the bcrypt module (entries found: %d ). " % self . _htpasswd_bcrypt_use ) from e
else :
2025-03-19 06:17:34 +01:00
self . _has_bcrypt = True
2025-03-08 17:26:28 +01:00
if self . _encryption == " autodetect " :
2024-12-31 17:09:21 +01:00
if self . _htpasswd_bcrypt_use == 0 :
2025-03-08 17:26:28 +01:00
logger . info ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' and bycrypt module found, but currently not required " , self . _encryption )
2024-12-31 17:09:21 +01:00
else :
2025-03-08 17:26:28 +01:00
logger . info ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' and bycrypt module found (bcrypt entries found: %d ) " , self . _encryption , self . _htpasswd_bcrypt_use )
if self . _encryption == " bcrypt " :
2024-03-12 06:08:26 +01:00
self . _verify = functools . partial ( self . _bcrypt , bcrypt )
else :
self . _verify = self . _autodetect
2025-03-19 06:17:34 +01:00
if self . _htpasswd_bcrypt_use :
self . _verify_bcrypt = functools . partial ( self . _bcrypt , bcrypt )
2025-04-29 19:18:27 +02:00
if self . _encryption == " argon2 " or self . _encryption == " autodetect " :
try :
import argon2
from passlib . hash import argon2
except ImportError as e :
if ( self . _encryption == " autodetect " ) and ( self . _htpasswd_argon2_use == 0 ) :
logger . warning ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' which can require argon2 module, but currently no entries found " , self . _encryption )
else :
raise RuntimeError (
" The htpasswd encryption method ' argon2 ' or ' autodetect ' requires "
" the argon2 module (entries found: %d ). " % self . _htpasswd_argon2_use ) from e
else :
self . _has_argon2 = True
if self . _encryption == " autodetect " :
if self . _htpasswd_argon2_use == 0 :
logger . info ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' and argon2 module found, but currently not required " , self . _encryption )
else :
logger . info ( " auth htpasswd encryption is ' radicale.auth.htpasswd_encryption. %s ' and argon2 module found (argon2 entries found: %d ) " , self . _encryption , self . _htpasswd_argon2_use )
if self . _encryption == " argon2 " :
self . _verify = functools . partial ( self . _argon2 , argon2 )
else :
self . _verify = self . _autodetect
if self . _htpasswd_argon2_use :
self . _verify_argon2 = functools . partial ( self . _argon2 , argon2 )
2025-04-29 19:26:01 +02:00
if not hasattr ( self , ' _verify ' ) :
2020-01-19 18:01:13 +01:00
raise RuntimeError ( " The htpasswd encryption method %r is not "
2025-03-08 17:26:28 +01:00
" supported. " % self . _encryption )
2016-04-22 11:37:02 +09:00
2024-12-30 05:25:10 +01:00
def _plain ( self , hash_value : str , password : str ) - > tuple [ str , bool ] :
2016-05-18 22:41:05 +02:00
""" Check if ``hash_value`` and ``password`` match, plain method. """
2024-12-30 08:17:44 +01:00
return ( " PLAIN " , hmac . compare_digest ( hash_value . encode ( ) , password . encode ( ) ) )
2016-04-22 11:37:02 +09:00
2025-03-09 08:49:30 +01:00
def _plain_fallback ( self , method_orig , hash_value : str , password : str ) - > tuple [ str , bool ] :
""" Check if ``hash_value`` and ``password`` match, plain method / fallback in case of hash length is not matching on autodetection. """
info = " PLAIN/fallback as hash length not matching for " + method_orig + " : " + str ( len ( hash_value ) )
return ( info , hmac . compare_digest ( hash_value . encode ( ) , password . encode ( ) ) )
2024-12-30 05:25:10 +01:00
def _bcrypt ( self , bcrypt : Any , hash_value : str , password : str ) - > tuple [ str , bool ] :
2025-03-09 08:49:30 +01:00
if self . _encryption == " autodetect " and len ( hash_value ) != 60 :
return self . _plain_fallback ( " BCRYPT " , hash_value , password )
else :
return ( " BCRYPT " , bcrypt . checkpw ( password = password . encode ( ' utf-8 ' ) , hashed_password = hash_value . encode ( ) ) )
2016-04-22 11:37:02 +09:00
2025-04-29 19:18:27 +02:00
def _argon2 ( self , argon2 : Any , hash_value : str , password : str ) - > tuple [ str , bool ] :
return ( " ARGON2 " , argon2 . verify ( password , hash_value . strip ( ) ) )
2024-12-30 05:25:10 +01:00
def _md5apr1 ( self , hash_value : str , password : str ) - > tuple [ str , bool ] :
2025-03-09 08:49:30 +01:00
if self . _encryption == " autodetect " and len ( hash_value ) != 37 :
return self . _plain_fallback ( " MD5-APR1 " , hash_value , password )
else :
return ( " MD5-APR1 " , apr_md5_crypt . verify ( password , hash_value . strip ( ) ) )
2016-04-22 11:37:02 +09:00
2024-12-30 05:25:10 +01:00
def _sha256 ( self , hash_value : str , password : str ) - > tuple [ str , bool ] :
2025-03-09 08:49:30 +01:00
if self . _encryption == " autodetect " and len ( hash_value ) != 63 :
return self . _plain_fallback ( " SHA-256 " , hash_value , password )
else :
return ( " SHA-256 " , sha256_crypt . verify ( password , hash_value . strip ( ) ) )
2024-03-12 06:08:26 +01:00
2024-12-30 05:25:10 +01:00
def _sha512 ( self , hash_value : str , password : str ) - > tuple [ str , bool ] :
2025-03-09 08:49:30 +01:00
if self . _encryption == " autodetect " and len ( hash_value ) != 106 :
return self . _plain_fallback ( " SHA-512 " , hash_value , password )
else :
return ( " SHA-512 " , sha512_crypt . verify ( password , hash_value . strip ( ) ) )
2024-03-12 06:08:26 +01:00
2024-12-30 05:25:10 +01:00
def _autodetect ( self , hash_value : str , password : str ) - > tuple [ str , bool ] :
2025-03-09 08:49:30 +01:00
if hash_value . startswith ( " $apr1$ " , 0 , 6 ) :
2024-03-12 06:08:26 +01:00
# MD5-APR1
return self . _md5apr1 ( hash_value , password )
2025-03-09 08:49:30 +01:00
elif re . match ( r " ^ \ $2(a|b|x|y)? \ $ " , hash_value ) :
2024-03-12 06:08:26 +01:00
# BCRYPT
return self . _verify_bcrypt ( hash_value , password )
2025-04-29 19:18:27 +02:00
elif re . match ( r " ^ \ $argon2(i|d|id) \ $ " , hash_value ) :
# ARGON2
return self . _verify_argon2 ( hash_value , password )
2025-03-09 08:49:30 +01:00
elif hash_value . startswith ( " $5$ " , 0 , 3 ) :
2024-03-12 06:08:26 +01:00
# SHA-256
return self . _sha256 ( hash_value , password )
2025-03-09 08:49:30 +01:00
elif hash_value . startswith ( " $6$ " , 0 , 3 ) :
2024-03-12 06:08:26 +01:00
# SHA-512
return self . _sha512 ( hash_value , password )
else :
return self . _plain ( hash_value , password )
2025-04-29 19:22:55 +02:00
def _read_htpasswd ( self , init : bool , suppress : bool ) - > Tuple [ bool , int , int , dict , int , int ] :
2024-12-31 16:14:38 +01:00
""" Read htpasswd file
2017-09-17 14:03:46 +02:00
2024-12-31 16:14:38 +01:00
init == True : stop on error
init == False : warn / skip on error and set mark to log reminder every interval
2025-01-01 16:31:31 +01:00
suppress == True : suppress warnings , change info to debug ( used in non - caching mode )
suppress == False : do not suppress warnings ( used in caching mode )
2017-09-17 14:03:46 +02:00
"""
2024-12-31 16:14:38 +01:00
htpasswd_ok = True
2024-12-31 17:09:21 +01:00
bcrypt_use = 0
2025-04-29 19:15:19 +02:00
argon2_use = 0
2025-01-01 16:31:31 +01:00
if ( init is True ) or ( suppress is True ) :
2024-12-31 16:14:38 +01:00
info = " Read "
else :
info = " Re-read "
2025-01-01 16:31:31 +01:00
if suppress is False :
logger . info ( " %s content of htpasswd file start: %r " , info , self . _filename )
else :
logger . debug ( " %s content of htpasswd file start: %r " , info , self . _filename )
htpasswd : dict [ str , str ] = dict ( )
entries = 0
duplicates = 0
errors = 0
2017-05-31 11:08:32 +02:00
try :
2020-01-19 18:29:29 +01:00
with open ( self . _filename , encoding = self . _encoding ) as f :
2024-12-31 16:14:38 +01:00
line_num = 0
2017-08-17 06:46:38 +02:00
for line in f :
2024-12-31 16:14:38 +01:00
line_num + = 1
2017-08-17 06:46:38 +02:00
line = line . rstrip ( " \n " )
2017-08-17 06:46:40 +02:00
if line . lstrip ( ) and not line . lstrip ( ) . startswith ( " # " ) :
2017-05-31 11:08:32 +02:00
try :
2024-12-31 18:26:43 +01:00
login , digest = line . split ( " : " , maxsplit = 1 )
2024-12-31 17:09:21 +01:00
skip = False
2024-12-31 16:14:38 +01:00
if login == " " or digest == " " :
if init is True :
raise ValueError ( " htpasswd file contains problematic line not matching <login>:<digest> in line: %d " % line_num )
else :
2025-01-01 16:31:31 +01:00
errors + = 1
2024-12-31 16:14:38 +01:00
logger . warning ( " htpasswd file contains problematic line not matching <login>:<digest> in line: %d (ignored) " , line_num )
htpasswd_ok = False
2024-12-31 17:09:21 +01:00
skip = True
2024-12-31 16:14:38 +01:00
else :
if htpasswd . get ( login ) :
duplicates + = 1
if init is True :
raise ValueError ( " htpasswd file contains duplicate login: ' %s ' " , login , line_num )
else :
logger . warning ( " htpasswd file contains duplicate login: ' %s ' (line: %d / ignored) " , login , line_num )
htpasswd_ok = False
2024-12-31 17:09:21 +01:00
skip = True
2024-12-31 16:14:38 +01:00
else :
2025-03-10 06:00:30 +01:00
if re . match ( r " ^ \ $2(a|b|x|y)? \ $ " , digest ) and len ( digest ) == 60 :
2024-12-31 17:09:21 +01:00
if init is True :
bcrypt_use + = 1
else :
if self . _has_bcrypt is False :
logger . warning ( " htpasswd file contains bcrypt digest login: ' %s ' (line: %d / ignored because module is not loaded) " , login , line_num )
skip = True
htpasswd_ok = False
2025-04-29 19:18:27 +02:00
if re . match ( r " ^ \ $argon2(i|d|id) \ $ " , digest ) :
if init is True :
argon2_use + = 1
else :
if self . _has_argon2 is False :
logger . warning ( " htpasswd file contains argon2 digest login: ' %s ' (line: %d / ignored because module is not loaded) " , login , line_num )
skip = True
htpasswd_ok = False
2024-12-31 17:09:21 +01:00
if skip is False :
htpasswd [ login ] = digest
entries + = 1
2017-05-31 11:08:32 +02:00
except ValueError as e :
2024-12-31 16:14:38 +01:00
if init is True :
raise RuntimeError ( " Invalid htpasswd file %r : %s " % ( self . _filename , e ) ) from e
2017-05-31 11:08:32 +02:00
except OSError as e :
2024-12-31 16:14:38 +01:00
if init is True :
raise RuntimeError ( " Failed to load htpasswd file %r : %s " % ( self . _filename , e ) ) from e
else :
2024-12-31 18:26:43 +01:00
logger . warning ( " Failed to load htpasswd file on re-read: %r " % self . _filename )
2024-12-31 16:14:38 +01:00
htpasswd_ok = False
2025-01-01 16:31:31 +01:00
htpasswd_size = os . stat ( self . _filename ) . st_size
htpasswd_mtime_ns = os . stat ( self . _filename ) . st_mtime_ns
if suppress is False :
logger . info ( " %s content of htpasswd file done: %r (entries: %d , duplicates: %d , errors: %d ) " , info , self . _filename , entries , duplicates , errors )
2024-12-31 16:14:38 +01:00
else :
2025-01-01 16:31:31 +01:00
logger . debug ( " %s content of htpasswd file done: %r (entries: %d , duplicates: %d , errors: %d ) " , info , self . _filename , entries , duplicates , errors )
2024-12-31 16:14:38 +01:00
if htpasswd_ok is True :
self . _htpasswd_not_ok_time = 0
else :
self . _htpasswd_not_ok_time = time . time ( )
2025-04-29 19:15:19 +02:00
return ( htpasswd_ok , bcrypt_use , argon2_use , htpasswd , htpasswd_size , htpasswd_mtime_ns )
2024-12-31 16:14:38 +01:00
def _login ( self , login : str , password : str ) - > str :
""" Validate credentials.
Iterate through htpasswd credential file until login matches , extract
hash ( encrypted password ) and check hash against password ,
using the method specified in the Radicale config .
2025-01-02 08:08:22 +01:00
Optional : the content of the file is cached and live updates will be detected by
2024-12-31 16:14:38 +01:00
comparing mtime_ns and size
"""
2025-01-02 08:08:22 +01:00
login_ok = False
digest : str
2025-01-01 16:31:31 +01:00
if self . _htpasswd_cache is True :
# check and re-read file if required
2024-12-31 16:14:38 +01:00
with self . _lock :
2025-01-01 16:31:31 +01:00
htpasswd_size = os . stat ( self . _filename ) . st_size
htpasswd_mtime_ns = os . stat ( self . _filename ) . st_mtime_ns
if ( htpasswd_size != self . _htpasswd_size ) or ( htpasswd_mtime_ns != self . _htpasswd_mtime_ns ) :
2025-04-29 19:15:19 +02:00
( self . _htpasswd_ok , self . _htpasswd_bcrypt_use , self . _htpasswd_argon2_use , self . _htpasswd , self . _htpasswd_size , self . _htpasswd_mtime_ns ) = self . _read_htpasswd ( False , False )
2025-01-01 16:31:31 +01:00
self . _htpasswd_not_ok_time = 0
2025-01-02 08:08:22 +01:00
# log reminder of problemantic file every interval
current_time = time . time ( )
if ( self . _htpasswd_ok is False ) :
if ( self . _htpasswd_not_ok_time > 0 ) :
if ( current_time - self . _htpasswd_not_ok_time ) > self . _htpasswd_not_ok_reminder_seconds :
logger . warning ( " htpasswd file still contains issues (REMINDER, check warnings in the past): %r " % self . _filename )
self . _htpasswd_not_ok_time = current_time
else :
self . _htpasswd_not_ok_time = current_time
if self . _htpasswd . get ( login ) :
digest = self . _htpasswd [ login ]
login_ok = True
2024-12-31 16:14:38 +01:00
else :
2025-01-01 16:31:31 +01:00
# read file on every request
2025-04-29 19:15:19 +02:00
( htpasswd_ok , htpasswd_bcrypt_use , htpasswd_argon2_use , htpasswd , htpasswd_size , htpasswd_mtime_ns ) = self . _read_htpasswd ( False , True )
2025-01-02 08:08:22 +01:00
if htpasswd . get ( login ) :
digest = htpasswd [ login ]
login_ok = True
2025-01-01 16:31:31 +01:00
2025-01-02 08:08:22 +01:00
if login_ok is True :
2025-03-08 17:27:02 +01:00
try :
( method , password_ok ) = self . _verify ( digest , password )
except ValueError as e :
2025-04-29 19:18:41 +02:00
logger . error ( " Login verification failed for user: ' %s ' (htpasswd/ %s ) with error ' %s ' " , login , self . _encryption , e )
2025-03-08 17:27:02 +01:00
return " "
2024-12-31 16:14:38 +01:00
if password_ok :
2025-03-09 08:50:53 +01:00
logger . debug ( " Login verification successful for user: ' %s ' (htpasswd/ %s / %s ) " , login , self . _encryption , method )
2024-12-31 16:14:38 +01:00
return login
else :
2025-03-09 08:50:53 +01:00
logger . warning ( " Login verification failed for user: ' %s ' (htpasswd/ %s / %s ) " , login , self . _encryption , method )
2024-12-31 16:14:38 +01:00
else :
2025-03-09 08:50:53 +01:00
logger . warning ( " Login verification user not found (htpasswd): ' %s ' " , login )
2018-08-16 07:59:58 +02:00
return " "