Skip to content

Commit 16859cf

Browse files
committed
feat(cognito-idp): make TOTP MFA work
Implement TOTP MFA using the existing cryptography API. This allows clients to behave correctly and use proper TOTP validation compared with previously where the UserCode was not handled. Warning: this implementation still uses the fixed constant "secret" and therefore should not be considered for anything other than testing. A solution implementing "real" TOTP was considered but would need storing the secret temporarily in the session during the auth process but the session object is unfortunately a tuple at this time.
1 parent 196ca4c commit 16859cf

File tree

5 files changed

+68
-10
lines changed

5 files changed

+68
-10
lines changed

moto/cognitoidp/models.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from .utils import (
3333
PAGINATION_MODEL,
3434
check_secret_hash,
35+
cognito_totp,
3536
expand_attrs,
3637
flatten_attrs,
3738
generate_id,
@@ -2187,16 +2188,22 @@ def associate_software_token(
21872188

21882189
raise NotAuthorizedError(access_token)
21892190

2190-
def verify_software_token(self, access_token: str, session: str) -> dict[str, str]:
2191+
def verify_software_token(
2192+
self, access_token: str, session: str, user_code: str
2193+
) -> dict[str, str]:
21912194
"""
21922195
The parameter UserCode has not yet been implemented
21932196
"""
2197+
totp = cognito_totp("asdfasdfasdf")
21942198
if session:
21952199
if session not in self.sessions:
21962200
raise ResourceNotFoundError(session)
21972201

21982202
username, user_pool = self.sessions[session]
21992203
user = self.admin_get_user(user_pool.id, username)
2204+
2205+
totp.verify(user_code.encode("utf-8"), int(time.time()))
2206+
22002207
user.token_verified = True
22012208

22022209
session = str(random.uuid4())
@@ -2208,6 +2215,8 @@ def verify_software_token(self, access_token: str, session: str) -> dict[str, st
22082215
_, username = user_pool.access_tokens[access_token]
22092216
user = self.admin_get_user(user_pool.id, username)
22102217

2218+
totp.verify(user_code.encode("utf-8"), int(time.time()))
2219+
22112220
user.token_verified = True
22122221

22132222
session = str(random.uuid4())
@@ -2453,9 +2462,11 @@ def associate_software_token(
24532462
backend = self._find_backend_by_access_token_or_session(access_token, session)
24542463
return backend.associate_software_token(access_token, session)
24552464

2456-
def verify_software_token(self, access_token: str, session: str) -> dict[str, str]:
2465+
def verify_software_token(
2466+
self, access_token: str, session: str, user_code: str
2467+
) -> dict[str, str]:
24572468
backend = self._find_backend_by_access_token_or_session(access_token, session)
2458-
return backend.verify_software_token(access_token, session)
2469+
return backend.verify_software_token(access_token, session, user_code)
24592470

24602471
def set_user_mfa_preference(
24612472
self,

moto/cognitoidp/responses.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,9 @@ def associate_software_token(self) -> ActionResult:
585585
def verify_software_token(self) -> ActionResult:
586586
access_token = self._get_param("AccessToken")
587587
session = self._get_param("Session")
588+
user_code = self._get_param("UserCode")
588589
result = self._get_region_agnostic_backend().verify_software_token(
589-
access_token, session
590+
access_token, session, user_code
590591
)
591592
return ActionResult(result)
592593

moto/cognitoidp/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import string
66
from typing import Any, Optional
77

8+
from cryptography.hazmat.primitives.hashes import SHA1
9+
from cryptography.hazmat.primitives.twofactor.totp import TOTP
10+
811
from moto.moto_api._internal import mock_random as random
912

1013
FORMATS = {
@@ -120,3 +123,19 @@ def _generate_id_hash(args: Any) -> str:
120123
hasher.update(str(arg).encode())
121124

122125
return hasher.hexdigest()
126+
127+
128+
def cognito_totp(key: str) -> TOTP:
129+
key_padded = key
130+
# Pad the secret if required before converting it to bytes
131+
padding = len(key) % 8
132+
if padding != 0:
133+
key_padded += "=" * (8 - padding)
134+
# https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html
135+
return TOTP(
136+
key=base64.b32decode(key_padded, casefold=True),
137+
length=6,
138+
algorithm=SHA1(),
139+
time_step=30,
140+
enforce_key_length=False,
141+
)

tests/test_cognitoidp/test_cognitoidp.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5248,9 +5248,13 @@ def test_setting_mfa():
52485248
result = authentication_flow(conn, auth_flow)
52495249

52505250
# Set MFA method
5251-
conn.associate_software_token(AccessToken=result["access_token"])
5251+
resp = conn.associate_software_token(AccessToken=result["access_token"])
5252+
secret_code = resp["SecretCode"]
5253+
totp = pyotp.TOTP(secret_code)
5254+
user_code = totp.now()
5255+
52525256
conn.verify_software_token(
5253-
AccessToken=result["access_token"], UserCode="123456"
5257+
AccessToken=result["access_token"], UserCode=user_code
52545258
)
52555259
conn.set_user_mfa_preference(
52565260
AccessToken=result["access_token"],
@@ -5332,8 +5336,12 @@ def test_admin_setting_mfa_totp_and_sms():
53325336
access_token = result["access_token"]
53335337
user_pool_id = result["user_pool_id"]
53345338
username = result["username"]
5335-
conn.associate_software_token(AccessToken=access_token)
5336-
conn.verify_software_token(AccessToken=access_token, UserCode="123456")
5339+
resp = conn.associate_software_token(AccessToken=access_token)
5340+
secret_code = resp["SecretCode"]
5341+
totp = pyotp.TOTP(secret_code)
5342+
user_code = totp.now()
5343+
5344+
conn.verify_software_token(AccessToken=access_token, UserCode=user_code)
53375345

53385346
# Set MFA TOTP and SMS methods
53395347
conn.admin_set_user_mfa_preference(
@@ -5368,8 +5376,11 @@ def test_admin_initiate_auth_when_token_totp_enabled():
53685376
username = result["username"]
53695377
client_id = result["client_id"]
53705378
password = result["password"]
5371-
conn.associate_software_token(AccessToken=access_token)
5372-
conn.verify_software_token(AccessToken=access_token, UserCode="123456")
5379+
resp = conn.associate_software_token(AccessToken=access_token)
5380+
secret_code = resp["SecretCode"]
5381+
totp = pyotp.TOTP(secret_code)
5382+
user_code = totp.now()
5383+
conn.verify_software_token(AccessToken=access_token, UserCode=user_code)
53735384

53745385
# Set MFA TOTP and SMS methods
53755386
conn.admin_set_user_mfa_preference(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import time
2+
3+
import pyotp
4+
5+
from moto.cognitoidp.utils import cognito_totp
6+
7+
8+
def test_cognito_totp():
9+
key = "asdfasdfasdf"
10+
client_totp = pyotp.TOTP(s=key)
11+
internal_totp = cognito_totp(key)
12+
13+
client_code = client_totp.now()
14+
internal_code = internal_totp.generate(int(time.time())).decode("utf-8")
15+
16+
assert internal_code == client_code

0 commit comments

Comments
 (0)