22import re
33import time
44from collections import OrderedDict
5- from typing import Any , Optional
5+ from typing import Any , Final , Optional
66
7+ from cryptography .hazmat .primitives .twofactor import InvalidToken
78from joserfc import jwk , jwt
89
910from moto .core .base_backend import BackendDict , BaseBackend
1516
1617from ..settings import (
1718 get_cognito_idp_user_pool_client_id_strategy ,
19+ get_cognito_idp_user_pool_enable_totp ,
1820 get_cognito_idp_user_pool_id_strategy ,
1921)
2022from .exceptions import (
2123 AliasExistsException ,
24+ CodeMismatchException ,
2225 ExpiredCodeException ,
2326 GroupExistsException ,
2427 InvalidParameterException ,
3235from .utils import (
3336 PAGINATION_MODEL ,
3437 check_secret_hash ,
38+ cognito_totp ,
3539 expand_attrs ,
3640 flatten_attrs ,
3741 generate_id ,
3842 validate_username_format ,
3943)
4044
45+ # FIXME: Should be per user and stored in the user's profile
46+ COGNITO_TOTP_MFA_SECRET : Final [str ] = "asdfasdfasdf"
47+
4148
4249class UserStatus (str , enum .Enum ):
4350 FORCE_CHANGE_PASSWORD = "FORCE_CHANGE_PASSWORD"
@@ -970,8 +977,12 @@ class CognitoIdpBackend(BaseBackend):
970977 In some cases, you need to have reproducible IDs for the user pool.
971978 For example, a single initialization before the start of integration tests.
972979
973- This behavior can be enabled by passing the environment variable: MOTO_COGNITO_IDP_USER_POOL_ID_STRATEGY=HASH.
974- Passing MOTO_COGNITO_IDP_USER_POOL_CLIENT_ID_STRATEGY=HASH enables the same logic for user pool clients.
980+ This behavior can be enabled by passing the environment variable: `MOTO_COGNITO_IDP_USER_POOL_ID_STRATEGY=HASH`.
981+ Passing `MOTO_COGNITO_IDP_USER_POOL_CLIENT_ID_STRATEGY=HASH` enables the same logic for user pool clients.
982+
983+ Support for MFA TOTP can be enabled by setting `MOTO_COGNITO_IDP_USER_POOL_ENABLE_TOTP=true`.
984+ Moto will validate the TOTP MFA provided by the user when registering MFA or subsequently authenticating.
985+ At this time, Moto uses a single fixed secret across all users.
975986 """
976987
977988 def __init__ (self , region_name : str , account_id : str ):
@@ -1720,6 +1731,16 @@ def respond_to_auth_challenge(
17201731 ):
17211732 raise NotAuthorizedError (secret_hash )
17221733
1734+ if (
1735+ challenge_name == "SOFTWARE_TOKEN_MFA"
1736+ and get_cognito_idp_user_pool_enable_totp ()
1737+ ):
1738+ totp = cognito_totp (COGNITO_TOTP_MFA_SECRET )
1739+ try :
1740+ totp .verify (mfa_code .encode ("utf-8" ), int (time .time ()))
1741+ except InvalidToken :
1742+ raise CodeMismatchException ("MFA Code Mismatch" )
1743+
17231744 del self .sessions [session ]
17241745 return self ._log_user_in (user_pool , client , username )
17251746
@@ -2173,7 +2194,7 @@ def initiate_auth(
21732194 def associate_software_token (
21742195 self , access_token : str , session : str
21752196 ) -> dict [str , str ]:
2176- secret_code = "asdfasdfasdf"
2197+ secret_code = COGNITO_TOTP_MFA_SECRET
21772198 if session :
21782199 if session in self .sessions :
21792200 return {"SecretCode" : secret_code , "Session" : session }
@@ -2188,16 +2209,25 @@ def associate_software_token(
21882209
21892210 raise NotAuthorizedError (access_token )
21902211
2191- def verify_software_token (self , access_token : str , session : str ) -> dict [ str , str ]:
2192- """
2193- The parameter UserCode has not yet been implemented
2194- """
2212+ def verify_software_token (
2213+ self , access_token : str , session : str , user_code : str , friendly_device_name : str
2214+ ) -> dict [ str , str ]:
2215+ totp = cognito_totp ( COGNITO_TOTP_MFA_SECRET )
21952216 if session :
21962217 if session not in self .sessions :
21972218 raise ResourceNotFoundError (session )
21982219
21992220 username , user_pool = self .sessions [session ]
22002221 user = self .admin_get_user (user_pool .id , username )
2222+
2223+ if get_cognito_idp_user_pool_enable_totp ():
2224+ try :
2225+ totp .verify (user_code .encode ("utf-8" ), int (time .time ()))
2226+ except InvalidToken :
2227+ raise CodeMismatchException (
2228+ f"Code mismatch ({ friendly_device_name } )"
2229+ )
2230+
22012231 user .token_verified = True
22022232
22032233 session = str (random .uuid4 ())
@@ -2209,6 +2239,14 @@ def verify_software_token(self, access_token: str, session: str) -> dict[str, st
22092239 _ , username = user_pool .access_tokens [access_token ]
22102240 user = self .admin_get_user (user_pool .id , username )
22112241
2242+ if get_cognito_idp_user_pool_enable_totp ():
2243+ try :
2244+ totp .verify (user_code .encode ("utf-8" ), int (time .time ()))
2245+ except InvalidToken :
2246+ raise CodeMismatchException (
2247+ f"Code mismatch ({ friendly_device_name } )"
2248+ )
2249+
22122250 user .token_verified = True
22132251
22142252 session = str (random .uuid4 ())
@@ -2454,9 +2492,17 @@ def associate_software_token(
24542492 backend = self ._find_backend_by_access_token_or_session (access_token , session )
24552493 return backend .associate_software_token (access_token , session )
24562494
2457- def verify_software_token (self , access_token : str , session : str ) -> dict [str , str ]:
2495+ def verify_software_token (
2496+ self ,
2497+ access_token : str ,
2498+ session : str ,
2499+ user_code : str ,
2500+ friendly_device_name : str ,
2501+ ) -> dict [str , str ]:
24582502 backend = self ._find_backend_by_access_token_or_session (access_token , session )
2459- return backend .verify_software_token (access_token , session )
2503+ return backend .verify_software_token (
2504+ access_token , session , user_code , friendly_device_name
2505+ )
24602506
24612507 def set_user_mfa_preference (
24622508 self ,
0 commit comments