mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-10-31 19:00:54 +00:00 
			
		
		
		
	Extract TOTPStrategy class to totp.py
* this decouples `TOTP` validation and storage logic from `auth` and moves it to `totp` * reduce `pyotp.validate#valid_window` from `2` to `1`
This commit is contained in:
		
							parent
							
								
									6594e19a1f
								
							
						
					
					
						commit
						ce70f44c58
					
				| @ -4,17 +4,10 @@ from flask import make_response | ||||
| 
 | ||||
| import utils, totp | ||||
| from mailconfig import get_mail_password, get_mail_user_privileges | ||||
| from mailconfig import get_two_factor_info, set_two_factor_last_used_token | ||||
| 
 | ||||
| DEFAULT_KEY_PATH   = '/var/lib/mailinabox/api.key' | ||||
| DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server' | ||||
| 
 | ||||
| class MissingTokenError(ValueError): | ||||
| 	pass | ||||
| 
 | ||||
| class BadTokenError(ValueError): | ||||
| 	pass | ||||
| 
 | ||||
| class KeyAuthService: | ||||
| 	"""Generate an API key for authenticating clients | ||||
| 
 | ||||
| @ -91,26 +84,10 @@ class KeyAuthService: | ||||
| 			if is_user_key: | ||||
| 				return (username, privs) | ||||
| 
 | ||||
| 			secret, last_token = get_two_factor_info(username, env) | ||||
| 			totp_strategy = totp.TOTPStrategy(email=username) | ||||
| 			# this will raise `totp.MissingTokenError` or `totp.BadTokenError` for bad requests | ||||
| 			totp_strategy.validate_request(request, env) | ||||
| 
 | ||||
| 			# 2FA is not enabled, we can skip further checks | ||||
| 			if secret == "" or secret == None: | ||||
| 				return (username, privs) | ||||
| 
 | ||||
| 			# If 2FA is enabled, raise if: | ||||
| 			# 1. no token is provided via `x-auth-token` | ||||
| 			# 2. a previously supplied token is used (to counter replay attacks) | ||||
| 			# 3. the token is invalid | ||||
| 			# in that case, we need to raise and indicate to the client to supply a TOTP | ||||
| 			token_header = request.headers.get('x-auth-token') | ||||
| 			if token_header == None or token_header == "": | ||||
| 				raise MissingTokenError("Two factor code missing (no x-auth-token supplied)") | ||||
| 
 | ||||
| 			# TODO: Should a token replay be handled as its own error? | ||||
| 			if token_header == last_token or totp.validate(secret, token_header) != True: | ||||
| 				raise BadTokenError("Two factor code incorrect") | ||||
| 
 | ||||
| 			set_two_factor_last_used_token(username, token_header, env) | ||||
| 			return (username, privs) | ||||
| 
 | ||||
| 	def get_user_credentials(self, email, pw, env): | ||||
|  | ||||
| @ -40,10 +40,10 @@ def authorized_personnel_only(viewfunc): | ||||
| 		error = None | ||||
| 		try: | ||||
| 			email, privs = auth_service.authenticate(request, env) | ||||
| 		except auth.MissingTokenError as e: | ||||
| 		except totp.MissingTokenError as e: | ||||
| 			privs = [] | ||||
| 			error = str(e) | ||||
| 		except auth.BadTokenError as e: | ||||
| 		except totp.BadTokenError as e: | ||||
| 			# Write a line in the log recording the failed login | ||||
| 			log_failed_login(request) | ||||
| 
 | ||||
| @ -128,7 +128,7 @@ def me(): | ||||
| 	# Is the caller authorized? | ||||
| 	try: | ||||
| 		email, privs = auth_service.authenticate(request, env) | ||||
| 	except auth.MissingTokenError as e: | ||||
| 	except totp.MissingTokenError as e: | ||||
| 		# Log the failed login | ||||
| 		log_failed_login(request) | ||||
| 
 | ||||
| @ -136,7 +136,7 @@ def me(): | ||||
| 			"status": "missing_token", | ||||
| 			"reason": str(e), | ||||
| 		}) | ||||
| 	except auth.BadTokenError as e: | ||||
| 	except totp.BadTokenError as e: | ||||
| 		# Log the failed login | ||||
| 		log_failed_login(request) | ||||
| 
 | ||||
|  | ||||
| @ -6,6 +6,7 @@ import struct | ||||
| import time | ||||
| import pyotp | ||||
| import qrcode | ||||
| from mailconfig import get_two_factor_info, set_two_factor_last_used_token | ||||
| 
 | ||||
| def get_secret(): | ||||
| 	return base64.b32encode(os.urandom(20)).decode('utf-8') | ||||
| @ -30,4 +31,42 @@ def validate(secret, token): | ||||
| 	@see https://tools.ietf.org/html/rfc4226#section-5.4 | ||||
| 	""" | ||||
| 	totp = pyotp.TOTP(secret) | ||||
| 	return totp.verify(token, valid_window=2) | ||||
| 	return totp.verify(token, valid_window=1) | ||||
| 
 | ||||
| class MissingTokenError(ValueError): | ||||
| 	pass | ||||
| 
 | ||||
| class BadTokenError(ValueError): | ||||
| 	pass | ||||
| 
 | ||||
| class TOTPStrategy(): | ||||
| 	def __init__(self, email): | ||||
| 		self.type = 'totp' | ||||
| 		self.email = email | ||||
| 
 | ||||
| 	def store_successful_login(self, token, env): | ||||
| 		return set_two_factor_last_used_token(self.email, token, env) | ||||
| 
 | ||||
| 	def validate_request(self, request, env): | ||||
| 		secret, mru_token = get_two_factor_info(self.email, env) | ||||
| 
 | ||||
| 		# 2FA is not enabled, we can skip further checks | ||||
| 		if secret == "" or secret == None: | ||||
| 			return True | ||||
| 
 | ||||
| 		# If 2FA is enabled, raise if: | ||||
| 		# 1. no token is provided via `x-auth-token` | ||||
| 		# 2. a previously supplied token is used (to counter replay attacks) | ||||
| 		# 3. the token is invalid | ||||
| 		# in that case, we need to raise and indicate to the client to supply a TOTP | ||||
| 		token_header = request.headers.get('x-auth-token') | ||||
| 
 | ||||
| 		if token_header == None or token_header == "": | ||||
| 			raise MissingTokenError("Two factor code missing (no x-auth-token supplied)") | ||||
| 
 | ||||
| 		# TODO: Should a token replay be handled as its own error? | ||||
| 		if token_header == mru_token or validate(secret, token_header) != True: | ||||
| 			raise BadTokenError("Two factor code incorrect") | ||||
| 
 | ||||
| 		self.store_successful_login(token_header, env) | ||||
| 		return True | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user