Description
Hi,
I believe I found a reproducible issue with Tapo MFA authentication in tplink-cloud-api 5.2.1.
When authenticating against the Tapo cloud with MFA enabled, TPLinkDeviceManager does not invoke the supplied mfa_callback.
Instead, Tapo authentication silently fails and TPLinkDeviceManager continues with the Kasa cloud only.
After investigating the installed v5.2.1 source and the actual API responses, it appears that the Tapo API returns the MFA requirement inside result.errorCode, while TPLinkApi.login() only checks the top-level response.error_code.
Environment
tplink-cloud-api: 5.2.1
- Python: 3.14
- OS: macOS
- TP-Link account region: CZ / eu-west-1
- MFA / 2FA: enabled
- Tapo cloud endpoint discovered:
https://n-euw1-wap-gw.tplinkcloud.com
Devices on the account include Tapo P110, P110M, H100, cameras and bulbs.
Expected behavior
Based on the README, this should call the MFA callback:
def handle_mfa(mfa_type, email):
return input(f"Enter MFA code sent to {email}: ")
manager = TPLinkDeviceManager(
username=username,
password=password,
mfa_callback=handle_mfa,
)
I expected:
- Tapo login
- MFA challenge detected
mfa_callback() called
- MFA verification completed
- Tapo token obtained
- Tapo devices queried
Actual behavior
The callback is never called.
TPLinkDeviceManager.get_devices() returns only the devices obtained through the Kasa cloud path.
Using verbose=True, I found that the Tapo login itself returns an MFA challenge successfully.
Simplified/redacted response:
{
"error_code": 0,
"result": {
"errorCode": "-20677",
"MFAProcessId": "<redacted>",
"supportedMFATypes": [
2,
1
],
"mfaEmail": "<redacted>",
"errorMsg": "MFA feature enabled"
}
}
The important detail seems to be:
while:
response.result["errorCode"] == "-20677"
Suspected cause
In the installed TPLinkApi.login() implementation:
response = self._request_post_v2(
regional_url, _PATH_LOGIN, login_body
)
error_code = response.error_code
if error_code == 0:
return response.result
if error_code == _ERR_MFA_REQUIRED:
...
Because the top-level error code is 0, the method immediately returns response.result.
Therefore this block is never reached:
if error_code == _ERR_MFA_REQUIRED:
As a result, mfa_callback is never called.
The returned dictionary does not contain a token, so in TPLinkDeviceManager._login_all():
self._tapo_token = tapo_result.get("token")
results in:
and Tapo device discovery is subsequently skipped.
Additional MFA issue: requesting the verification code
I also tested the MFA flow manually.
The initial Tapo login successfully returns:
MFAProcessId: present
supportedMFATypes: [2, 1]
Calling:
/api/v2/account/getEmailVC4TerminalMFA
with my initially attempted payload returned:
However, using the push MFA endpoint:
/api/v2/account/getPushVC4TerminalMFA
with:
{
"appType": api._app_type,
"cloudPassword": password,
"cloudUserName": username,
"terminalUUID": api._term_id,
}
successfully returned:
and a verification code was delivered through the Tapo app.
I then verified the code through:
/api/v2/account/checkMFACodeAndLogin
using:
{
"appType": api._app_type,
"cloudUserName": username,
"code": mfa_code,
"MFAProcessId": mfa_process_id,
"MFAType": 1,
"terminalBindEnabled": True,
}
This completed successfully:
MFA LOGIN SUCCESSFUL
Token received: YES
So the Tapo MFA flow itself works against the current API; the main issue appears to be how v5.2.1 detects and handles the MFA challenge.
Minimal reproduction
The following demonstrates that login() returns the MFA challenge as a normal result rather than invoking the callback:
from tplinkcloud import TPLinkDeviceManager
def handle_mfa(mfa_type, email):
print("MFA CALLBACK CALLED")
return input("Code: ")
username = input("TP-Link email: ")
password = input("TP-Link password: ")
manager = TPLinkDeviceManager(
include_tapo=True,
verbose=False,
)
result = manager._tapo_api.login(
username,
password,
mfa_callback=handle_mfa,
)
print("Keys:", list(result.keys()))
print("Has token:", "token" in result)
print("Inner errorCode:", result.get("errorCode"))
Result:
MFA CALLBACK CALLED
# <-- never printed
Keys:
[
'lockedMinutes',
'mfaEmail',
'failedAttempts',
'remainAttempts',
'errorCode',
'MFAProcessId',
'supportedMFATypes',
'email',
'errorMsg'
]
Has token: False
Inner errorCode: -20677
Possible fix
It seems TPLinkApi.login() may need to inspect the nested login result before treating top-level error_code == 0 as a successful authentication.
Something conceptually like:
response = self._request_post_v2(
regional_url,
_PATH_LOGIN,
login_body,
)
result = response.result or {}
inner_error_code = str(result.get("errorCode", "0"))
if response.error_code == 0 and inner_error_code == "0":
return result
if response.error_code == 0 and inner_error_code == str(_ERR_MFA_REQUIRED):
# perform MFA flow
...
The exact MFA request implementation may also need adjustment because the current _verify_mfa() does not appear to perform the preceding push/email verification-code request.
Verification after manual workaround
After completing the push MFA flow manually, I received a valid Tapo token and was able to call the Tapo cloud device-list API directly.
It returned all 15 top-level Tapo devices on my account, including:
- P110
- P110M
- H100
- Tapo bulbs
- Tapo cameras
So Tapo cloud discovery itself works once authentication has successfully completed.
I can provide additional sanitized API responses or test a patch if useful.
Thanks!
Description
Hi,
I believe I found a reproducible issue with Tapo MFA authentication in
tplink-cloud-api 5.2.1.When authenticating against the Tapo cloud with MFA enabled,
TPLinkDeviceManagerdoes not invoke the suppliedmfa_callback.Instead, Tapo authentication silently fails and
TPLinkDeviceManagercontinues with the Kasa cloud only.After investigating the installed v5.2.1 source and the actual API responses, it appears that the Tapo API returns the MFA requirement inside
result.errorCode, whileTPLinkApi.login()only checks the top-levelresponse.error_code.Environment
tplink-cloud-api: 5.2.1https://n-euw1-wap-gw.tplinkcloud.comDevices on the account include Tapo P110, P110M, H100, cameras and bulbs.
Expected behavior
Based on the README, this should call the MFA callback:
I expected:
mfa_callback()calledActual behavior
The callback is never called.
TPLinkDeviceManager.get_devices()returns only the devices obtained through the Kasa cloud path.Using
verbose=True, I found that the Tapo login itself returns an MFA challenge successfully.Simplified/redacted response:
{ "error_code": 0, "result": { "errorCode": "-20677", "MFAProcessId": "<redacted>", "supportedMFATypes": [ 2, 1 ], "mfaEmail": "<redacted>", "errorMsg": "MFA feature enabled" } }The important detail seems to be:
while:
Suspected cause
In the installed
TPLinkApi.login()implementation:Because the top-level error code is
0, the method immediately returnsresponse.result.Therefore this block is never reached:
As a result,
mfa_callbackis never called.The returned dictionary does not contain a token, so in
TPLinkDeviceManager._login_all():results in:
and Tapo device discovery is subsequently skipped.
Additional MFA issue: requesting the verification code
I also tested the MFA flow manually.
The initial Tapo login successfully returns:
Calling:
with my initially attempted payload returned:
However, using the push MFA endpoint:
with:
{ "appType": api._app_type, "cloudPassword": password, "cloudUserName": username, "terminalUUID": api._term_id, }successfully returned:
and a verification code was delivered through the Tapo app.
I then verified the code through:
using:
{ "appType": api._app_type, "cloudUserName": username, "code": mfa_code, "MFAProcessId": mfa_process_id, "MFAType": 1, "terminalBindEnabled": True, }This completed successfully:
So the Tapo MFA flow itself works against the current API; the main issue appears to be how v5.2.1 detects and handles the MFA challenge.
Minimal reproduction
The following demonstrates that
login()returns the MFA challenge as a normal result rather than invoking the callback:Result:
Possible fix
It seems
TPLinkApi.login()may need to inspect the nested login result before treating top-levelerror_code == 0as a successful authentication.Something conceptually like:
The exact MFA request implementation may also need adjustment because the current
_verify_mfa()does not appear to perform the preceding push/email verification-code request.Verification after manual workaround
After completing the push MFA flow manually, I received a valid Tapo token and was able to call the Tapo cloud device-list API directly.
It returned all 15 top-level Tapo devices on my account, including:
So Tapo cloud discovery itself works once authentication has successfully completed.
I can provide additional sanitized API responses or test a patch if useful.
Thanks!