Skip to content

Commit

Permalink
Add support for developer API Key (#140)
Browse files Browse the repository at this point in the history
* Add support for developer API Key

* updated logic for api key

---------

Co-authored-by: Shaun Tarves <[email protected]>
  • Loading branch information
mrlt8 and shauntarves authored Jul 22, 2023
1 parent 5b5cc38 commit e63e8da
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 12 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ client = Client(email=os.environ['WYZE_EMAIL'], password=os.environ['WYZE_PASSWO
...
```

##### Wyze API Key/ID Support

Visit the Wyze developer API portal to generate an API ID/KEY: https://developer-api-console.wyze.com/#/apikey/view

```python
import os
from wyze_sdk import Client

response = Client().login(
email=os.environ['WYZE_EMAIL'],
password=os.environ['WYZE_PASSWORD'],
key_id=os.environ['WYZE_KEY_ID'],
api_key=os.environ['WYZE_API_KEY']
)
...
```

##### Multi-Factor Authentication (2FA) Support

If your Wyze account has multi-factor authentication (2FA) enabled, you may be prompted for your 2FA code when authenticating via either supported method described above. If you wish to automate the MFA interaction, both the `Client` constructor and the `login()` method accept `totp_key` as input. If the TOTP key is provided, the MFA prompt should not appear.
Expand Down
38 changes: 34 additions & 4 deletions wyze_sdk/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def __init__(
refresh_token: Optional[str] = None,
email: Optional[str] = None,
password: Optional[str] = None,
key_id: Optional[str] = None,
api_key: Optional[str] = None,
totp_key: Optional[str] = None,
base_url: Optional[str] = None,
timeout: int = 30,
Expand All @@ -55,6 +57,10 @@ def __init__(
self._email = None if email is None else email.strip()
#: An unencrypted string specifying the account password.
self._password = None if password is None else password.strip()
# A string used for API-based requests
self._key_id = key_id.strip() if key_id else None
# A string used for API-based requests
self._api_key = api_key.strip() if api_key else None
#: An unencrypted string specifying the TOTP Key for automatic TOTP 2FA verification code generation.
self._totp_key = None if totp_key is None else totp_key.strip()
#: An optional string representing the API base URL. **This should not be used except for when running tests.**
Expand Down Expand Up @@ -136,8 +142,10 @@ def login(
self,
email: str = None,
password: str = None,
key_id: str = None,
api_key: str = None,
totp_key: Optional[str] = None,
) -> WyzeResponse:
) -> WyzeResponse:
"""
Exchanges email and password for an ``access_token`` and a ``refresh_token``, which
are stored in this client. The tokens will be used for all subsequent requests
Expand All @@ -156,13 +164,35 @@ def login(
self._email = email.strip()
if password is not None:
self._password = password.strip()
if key_id is not None:
self._key_id = key_id.strip()
if api_key is not None:
self._api_key = api_key.strip()
if totp_key is not None:
self._totp_key = totp_key.strip()
if self._email is None or self._password is None:
raise WyzeClientConfigurationError("must provide email and password")
self._logger.debug(f"access token not provided, attempting to login as {self._email}")
response = self._auth_client().user_login(email=self._email, password=self._password, totp_key=self._totp_key)
self._update_session(access_token=response["access_token"], refresh_token=response["refresh_token"], user_id=response["user_id"])
if self._key_id is None or self._api_key is None:
raise WyzeClientConfigurationError(
"Must provide a Wyze API key and id.\n\n" +
"As of July 2023, users must provide an api key and key id to create an access token. " +
"For more information, please visit https://support.wyze.com/hc/en-us/articles/16129834216731."
)
self._logger.debug(
f"access token not provided, attempting to login as {self._email}"
)
response = self._auth_client().user_login(
email=self._email,
password=self._password,
key_id=self._key_id,
api_key=self._api_key,
totp_key=self._totp_key,
)
self._update_session(
access_token=response["access_token"],
refresh_token=response["refresh_token"],
user_id=response["user_id"],
)
return response

def refresh_token(self) -> WyzeResponse:
Expand Down
32 changes: 24 additions & 8 deletions wyze_sdk/service/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from typing import Dict, Optional

from mintotp import totp

from wyze_sdk import version
from wyze_sdk.signature import RequestVerifier

from .base import ExServiceClient, WyzeResponse
Expand Down Expand Up @@ -62,18 +64,32 @@ def api_call(
nonce=nonce,
)

def user_login(self, *, email: str, password: str, totp_key: Optional[str] = None, **kwargs) -> WyzeResponse:
def user_login(
self,
*,
email: str,
password: str,
key_id: Optional[str] = None,
api_key: Optional[str] = None,
totp_key: Optional[str] = None,
**kwargs,
) -> WyzeResponse:
nonce = self.request_verifier.clock.nonce()
password = self.request_verifier.md5_string(
self.request_verifier.md5_string(self.request_verifier.md5_string(password))
)
kwargs.update({
'nonce': str(nonce),
'email': email,
'password': password
})
response = self.api_call('/user/login', json=kwargs, nonce=nonce)
if response['access_token']:
kwargs.update({"nonce": str(nonce), "email": email, "password": password})
api_headers = {
"keyid": key_id,
"apikey": api_key,
"user-agent": f"wyze-sdk-{version.__version__}",
}

response = self.api_call(
'/api/user/login', json=kwargs, request_specific_headers=api_headers, nonce=nonce
)

if response["access_token"]:
return response

if 'TotpVerificationCode' in response.get('mfa_options'):
Expand Down

0 comments on commit e63e8da

Please sign in to comment.