2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2018-08-28 16:19:36 +02:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
2024-06-07 06:46:16 +02:00
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
2025-01-01 15:47:22 +01:00
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
2018-08-28 16:19:36 +02: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 module .
2018-08-28 16:19:36 +02:00
2020-01-12 23:32:28 +01:00
Authentication is based on usernames and passwords . If something more
advanced is needed an external WSGI server or reverse proxy can be used
( see ` ` remote_user ` ` or ` ` http_x_remote_user ` ` backend ) .
2018-08-28 16:19:36 +02:00
2020-01-12 23:32:28 +01:00
Take a look at the class ` ` BaseAuth ` ` if you want to implement your own .
2018-08-28 16:19:36 +02:00
"""
2024-12-30 08:17:15 +01:00
import hashlib
2024-12-31 16:13:52 +01:00
import threading
2024-12-31 18:26:43 +01:00
import time
2024-12-25 21:56:04 +03:00
from typing import Sequence , Set , Tuple , Union , final
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
from radicale import config , types , utils
2024-06-07 08:35:46 +02:00
from radicale . log import logger
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
INTERNAL_TYPES : Sequence [ str ] = ( " none " , " remote_user " , " http_x_remote_user " ,
2024-06-07 06:45:39 +02:00
" denyall " ,
2024-08-25 14:11:48 +02:00
" htpasswd " ,
2024-10-30 10:33:10 -10:00
" ldap " ,
2025-01-16 06:02:06 +01:00
" imap " ,
2025-02-02 09:01:58 +01:00
" oauth2 " ,
2025-02-22 17:49:36 +01:00
" pam " ,
2024-10-30 10:33:10 -10:00
" dovecot " )
2018-08-28 16:19:36 +02:00
2025-01-16 06:02:06 +01:00
CACHE_LOGIN_TYPES : Sequence [ str ] = (
" dovecot " ,
" ldap " ,
" htpasswd " ,
" imap " ,
2025-02-02 09:01:58 +01:00
" oauth2 " ,
2025-02-22 17:49:36 +01:00
" pam " ,
2025-01-16 06:02:06 +01:00
)
2025-01-13 23:10:18 -08:00
AUTH_SOCKET_FAMILY : Sequence [ str ] = ( " AF_UNIX " , " AF_INET " , " AF_INET6 " )
2021-07-26 20:56:46 +02:00
def load ( configuration : " config.Configuration " ) - > " BaseAuth " :
2020-01-14 22:43:48 +01:00
""" Load the authentication module chosen in configuration. """
2024-06-07 08:35:46 +02:00
if configuration . get ( " auth " , " type " ) == " none " :
2024-06-09 08:32:45 +02:00
logger . warning ( " No user authentication is selected: ' [auth] type=none ' (insecure) " )
2024-06-07 12:35:21 +02:00
if configuration . get ( " auth " , " type " ) == " denyall " :
2024-06-09 08:32:45 +02:00
logger . warning ( " All access is blocked by: ' [auth] type=denyall ' " )
2021-07-26 20:56:46 +02:00
return utils . load_plugin ( INTERNAL_TYPES , " auth " , " Auth " , BaseAuth ,
configuration )
2018-08-28 16:19:36 +02:00
2024-12-25 22:28:01 +03:00
2018-08-28 16:19:36 +02:00
class BaseAuth :
2021-07-26 20:56:46 +02:00
2024-09-11 14:13:06 +02:00
_ldap_groups : Set [ str ] = set ( [ ] )
2024-04-17 18:31:51 +03:00
_lc_username : bool
2024-12-14 09:25:36 +01:00
_uc_username : bool
2024-07-18 06:50:29 +02:00
_strip_domain : bool
2025-01-02 22:33:54 +01:00
_auth_delay : float
_failed_auth_delay : float
2024-12-31 08:11:19 +01:00
_type : str
2024-12-30 08:17:15 +01:00
_cache_logins : bool
2024-12-31 07:57:54 +01:00
_cache_successful : dict # login -> (digest, time_ns)
_cache_successful_logins_expiry : int
2024-12-31 16:13:52 +01:00
_cache_failed : dict # digest_failed -> (time_ns, login)
2024-12-31 07:57:54 +01:00
_cache_failed_logins_expiry : int
_cache_failed_logins_salt_ns : int # persistent over runtime
2024-12-31 16:13:52 +01:00
_lock : threading . Lock
2022-02-21 17:15:21 +01:00
2021-07-26 20:56:46 +02:00
def __init__ ( self , configuration : " config.Configuration " ) - > None :
2020-01-13 15:51:10 +01:00
""" Initialize BaseAuth.
` ` configuration ` ` see ` ` radicale . config ` ` module .
The ` ` configuration ` ` must not change during the lifetime of
this object , it is kept as an internal reference .
"""
2018-08-28 16:19:36 +02:00
self . configuration = configuration
2024-04-17 18:31:51 +03:00
self . _lc_username = configuration . get ( " auth " , " lc_username " )
2024-12-14 09:25:36 +01:00
self . _uc_username = configuration . get ( " auth " , " uc_username " )
2024-07-18 06:50:29 +02:00
self . _strip_domain = configuration . get ( " auth " , " strip_domain " )
2024-12-14 09:25:36 +01:00
logger . info ( " auth.strip_domain: %s " , self . _strip_domain )
logger . info ( " auth.lc_username: %s " , self . _lc_username )
logger . info ( " auth.uc_username: %s " , self . _uc_username )
if self . _lc_username is True and self . _uc_username is True :
raise RuntimeError ( " auth.lc_username and auth.uc_username cannot be enabled together " )
2025-01-02 22:33:54 +01:00
self . _auth_delay = configuration . get ( " auth " , " delay " )
logger . info ( " auth.delay: %f " , self . _auth_delay )
2025-01-03 07:14:32 +01:00
self . _failed_auth_delay = 0
2025-01-02 23:17:34 +01:00
self . _lock = threading . Lock ( )
2024-12-31 07:57:54 +01:00
# cache_successful_logins
2024-12-31 08:11:19 +01:00
self . _cache_logins = configuration . get ( " auth " , " cache_logins " )
self . _type = configuration . get ( " auth " , " type " )
2025-01-16 06:02:06 +01:00
if ( self . _type in CACHE_LOGIN_TYPES ) or ( self . _cache_logins is False ) :
2024-12-31 08:11:19 +01:00
logger . info ( " auth.cache_logins: %s " , self . _cache_logins )
else :
logger . info ( " auth.cache_logins: %s (but not required for type ' %s ' and disabled therefore) " , self . _cache_logins , self . _type )
self . _cache_logins = False
2024-12-30 08:17:15 +01:00
if self . _cache_logins is True :
2024-12-31 08:11:19 +01:00
self . _cache_successful_logins_expiry = configuration . get ( " auth " , " cache_successful_logins_expiry " )
if self . _cache_successful_logins_expiry < 0 :
raise RuntimeError ( " self._cache_successful_logins_expiry cannot be < 0 " )
self . _cache_failed_logins_expiry = configuration . get ( " auth " , " cache_failed_logins_expiry " )
if self . _cache_failed_logins_expiry < 0 :
raise RuntimeError ( " self._cache_failed_logins_expiry cannot be < 0 " )
2024-12-31 07:57:54 +01:00
logger . info ( " auth.cache_successful_logins_expiry: %s seconds " , self . _cache_successful_logins_expiry )
logger . info ( " auth.cache_failed_logins_expiry: %s seconds " , self . _cache_failed_logins_expiry )
2024-12-31 08:11:19 +01:00
# cache init
self . _cache_successful = dict ( )
self . _cache_failed = dict ( )
self . _cache_failed_logins_salt_ns = time . time_ns ( )
2024-12-30 08:17:15 +01:00
def _cache_digest ( self , login : str , password : str , salt : str ) - > str :
h = hashlib . sha3_512 ( )
h . update ( salt . encode ( ) )
h . update ( login . encode ( ) )
h . update ( password . encode ( ) )
2024-12-30 05:25:10 +01:00
return str ( h . digest ( ) )
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def get_external_login ( self , environ : types . WSGIEnviron ) - > Union [
Tuple [ ( ) ] , Tuple [ str , str ] ] :
2018-08-28 16:19:36 +02:00
""" Optionally provide the login and password externally.
` ` environ ` ` a dict with the WSGI environment
If ` ` ( ) ` ` is returned , Radicale handles HTTP authentication .
Otherwise , returns a tuple ` ` ( login , password ) ` ` . For anonymous users
` ` login ` ` must be ` ` " " ` ` .
"""
return ( )
2024-04-17 18:31:51 +03:00
def _login ( self , login : str , password : str ) - > str :
2018-08-28 16:19:36 +02:00
""" Check credentials and map login to internal user
` ` login ` ` the login name
` ` password ` ` the password
2022-01-07 23:54:34 +01:00
Returns the username or ` ` " " ` ` for invalid credentials .
2018-08-28 16:19:36 +02:00
"""
raise NotImplementedError
2024-04-17 18:31:51 +03:00
2025-01-02 23:17:34 +01:00
def _sleep_for_constant_exec_time ( self , time_ns_begin : int ) :
2025-01-03 07:14:32 +01:00
""" Sleep some time to reach a constant execution time for failed logins
Independent of time required by external backend or used digest methods
2025-01-02 22:33:54 +01:00
Increase final execution time in case initial limit exceeded
2025-01-03 07:14:32 +01:00
See also issue 591
2025-01-02 22:33:54 +01:00
"""
time_delta = ( time . time_ns ( ) - time_ns_begin ) / 1000 / 1000 / 1000
2025-01-03 07:14:32 +01:00
with self . _lock :
# avoid that another thread is changing global value at the same time
failed_auth_delay = self . _failed_auth_delay
failed_auth_delay_old = failed_auth_delay
if time_delta > failed_auth_delay :
# set new
failed_auth_delay = time_delta
# store globally
self . _failed_auth_delay = failed_auth_delay
if ( failed_auth_delay_old != failed_auth_delay ) :
logger . debug ( " Failed login constant execution time need increase of failed_auth_delay: %.9f -> %.9f sec " , failed_auth_delay_old , failed_auth_delay )
# sleep == 0
else :
sleep = failed_auth_delay - time_delta
logger . debug ( " Failed login constant exection time alignment, sleeping: %.9f sec " , sleep )
time . sleep ( sleep )
2025-01-02 22:33:54 +01:00
2024-12-25 21:56:04 +03:00
@final
2025-01-01 16:30:34 +01:00
def login ( self , login : str , password : str ) - > Tuple [ str , str ] :
2025-01-02 22:33:54 +01:00
time_ns_begin = time . time_ns ( )
2025-01-01 16:30:34 +01:00
result_from_cache = False
2024-07-18 06:50:29 +02:00
if self . _lc_username :
login = login . lower ( )
2024-12-14 09:25:36 +01:00
if self . _uc_username :
login = login . upper ( )
2024-07-18 06:50:29 +02:00
if self . _strip_domain :
login = login . split ( ' @ ' ) [ 0 ]
2024-12-30 08:17:15 +01:00
if self . _cache_logins is True :
# time_ns is also used as salt
result = " "
digest = " "
time_ns = time . time_ns ( )
2024-12-31 16:13:52 +01:00
# cleanup failed login cache to avoid out-of-memory
cache_failed_entries = len ( self . _cache_failed )
if cache_failed_entries > 0 :
logger . debug ( " Login failed cache investigation start (entries: %d ) " , cache_failed_entries )
self . _lock . acquire ( )
cache_failed_cleanup = dict ( )
for digest in self . _cache_failed :
( time_ns_cache , login_cache ) = self . _cache_failed [ digest ]
age_failed = int ( ( time_ns - time_ns_cache ) / 1000 / 1000 / 1000 )
if age_failed > self . _cache_failed_logins_expiry :
cache_failed_cleanup [ digest ] = ( login_cache , age_failed )
cache_failed_cleanup_entries = len ( cache_failed_cleanup )
logger . debug ( " Login failed cache cleanup start (entries: %d ) " , cache_failed_cleanup_entries )
if cache_failed_cleanup_entries > 0 :
for digest in cache_failed_cleanup :
( login , age_failed ) = cache_failed_cleanup [ digest ]
logger . debug ( " Login failed cache entry for user+password expired: ' %s ' (age: %d > %d sec) " , login_cache , age_failed , self . _cache_failed_logins_expiry )
del self . _cache_failed [ digest ]
self . _lock . release ( )
logger . debug ( " Login failed cache investigation finished " )
# check for cache failed login
2024-12-31 07:57:54 +01:00
digest_failed = login + " : " + self . _cache_digest ( login , password , str ( self . _cache_failed_logins_salt_ns ) )
if self . _cache_failed . get ( digest_failed ) :
2024-12-31 16:13:52 +01:00
# login+password found in cache "failed" -> shortcut return
( time_ns_cache , login_cache ) = self . _cache_failed [ digest ]
2024-12-31 07:57:54 +01:00
age_failed = int ( ( time_ns - time_ns_cache ) / 1000 / 1000 / 1000 )
2024-12-31 16:13:52 +01:00
logger . debug ( " Login failed cache entry for user+password found: ' %s ' (age: %d sec) " , login_cache , age_failed )
2025-01-03 07:14:32 +01:00
self . _sleep_for_constant_exec_time ( time_ns_begin )
2025-01-01 16:30:34 +01:00
return ( " " , self . _type + " / cached " )
2024-12-31 07:57:54 +01:00
if self . _cache_successful . get ( login ) :
# login found in cache "successful"
( digest_cache , time_ns_cache ) = self . _cache_successful [ login ]
2024-12-30 08:17:15 +01:00
digest = self . _cache_digest ( login , password , str ( time_ns_cache ) )
if digest == digest_cache :
2024-12-31 07:57:54 +01:00
age_success = int ( ( time_ns - time_ns_cache ) / 1000 / 1000 / 1000 )
if age_success > self . _cache_successful_logins_expiry :
logger . debug ( " Login successful cache entry for user+password found but expired: ' %s ' (age: %d > %d sec) " , login , age_success , self . _cache_successful_logins_expiry )
# delete expired success from cache
del self . _cache_successful [ login ]
2024-12-30 08:17:15 +01:00
digest = " "
else :
2024-12-31 07:57:54 +01:00
logger . debug ( " Login successful cache entry for user+password found: ' %s ' (age: %d sec) " , login , age_success )
2024-12-30 08:17:15 +01:00
result = login
2025-01-01 16:30:34 +01:00
result_from_cache = True
2024-12-30 08:17:15 +01:00
else :
2024-12-31 07:57:54 +01:00
logger . debug ( " Login successful cache entry for user+password not matching: ' %s ' " , login )
2024-12-30 08:17:15 +01:00
else :
2024-12-31 07:57:54 +01:00
# login not found in cache, caculate always to avoid timing attacks
2024-12-30 08:17:15 +01:00
digest = self . _cache_digest ( login , password , str ( time_ns ) )
if result == " " :
2024-12-31 07:57:54 +01:00
# verify login+password via configured backend
logger . debug ( " Login verification for user+password via backend: ' %s ' " , login )
2024-12-30 08:17:15 +01:00
result = self . _login ( login , password )
2024-12-30 05:25:10 +01:00
if result != " " :
2024-12-31 07:57:54 +01:00
logger . debug ( " Login successful for user+password via backend: ' %s ' " , login )
2024-12-30 05:25:10 +01:00
if digest == " " :
2024-12-30 08:17:15 +01:00
# successful login, but expired, digest must be recalculated
digest = self . _cache_digest ( login , password , str ( time_ns ) )
# store successful login in cache
2024-12-31 16:13:52 +01:00
self . _lock . acquire ( )
2024-12-31 07:57:54 +01:00
self . _cache_successful [ login ] = ( digest , time_ns )
2024-12-31 16:13:52 +01:00
self . _lock . release ( )
2024-12-31 07:57:54 +01:00
logger . debug ( " Login successful cache for user set: ' %s ' " , login )
if self . _cache_failed . get ( digest_failed ) :
logger . debug ( " Login failed cache for user cleared: ' %s ' " , login )
del self . _cache_failed [ digest_failed ]
else :
logger . debug ( " Login failed for user+password via backend: ' %s ' " , login )
2024-12-31 16:13:52 +01:00
self . _lock . acquire ( )
self . _cache_failed [ digest_failed ] = ( time_ns , login )
self . _lock . release ( )
2024-12-31 07:57:54 +01:00
logger . debug ( " Login failed cache for user set: ' %s ' " , login )
2025-01-01 16:30:34 +01:00
if result_from_cache is True :
2025-01-02 22:33:54 +01:00
if result == " " :
2025-01-03 07:14:32 +01:00
self . _sleep_for_constant_exec_time ( time_ns_begin )
2025-01-01 16:30:34 +01:00
return ( result , self . _type + " / cached " )
else :
2025-01-02 22:33:54 +01:00
if result == " " :
2025-01-03 07:14:32 +01:00
self . _sleep_for_constant_exec_time ( time_ns_begin )
2025-01-01 16:30:34 +01:00
return ( result , self . _type )
2024-12-30 08:17:15 +01:00
else :
2025-01-02 22:33:54 +01:00
# self._cache_logins is False
result = self . _login ( login , password )
if result == " " :
2025-01-03 07:14:32 +01:00
self . _sleep_for_constant_exec_time ( time_ns_begin )
2025-01-02 22:33:54 +01:00
return ( result , self . _type )