Skip to content

Commit 3f80cb2

Browse files
authored
Re-work authentication to need only username/password (bachya#323)
* Re-work authentication to need only username/password * Linting
1 parent 5cf1fa6 commit 3f80cb2

29 files changed

Lines changed: 3452 additions & 477 deletions

‎docs/images/ss-login-screen.png‎

-100 KB
Binary file not shown.
-63.4 KB
Binary file not shown.
-159 KB
Binary file not shown.

‎docs/usage.rst‎

Lines changed: 62 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -31,116 +31,99 @@ the current system state.
3131
Accessing the API
3232
-----------------
3333

34-
Starting in 2021, SimpliSafe™ began to implement an OAuth-based form of authentication.
35-
To use this library, you must handshake with the SimpliSafe™ API; although this process
36-
cannot be 100% accomplished programmatically, the procedure is fairly straightforward.
34+
First, authenticate using your SimpliSafe username/email and password:
3735

38-
Authentication
39-
**************
40-
41-
``simplipy`` comes with a helper script to get you started. To use it, follow these
42-
steps from a command line:
43-
44-
1. Clone the ``simplipy`` Git repo and ``cd`` into it:
45-
46-
.. code:: bash
47-
48-
$ git clone https://github.com/bachya/simplisafe-python.git
49-
$ cd simplisafe-python/
50-
51-
2. Set up and activate a Python virtual environment:
52-
53-
.. code:: bash
54-
55-
$ python3 -m virtualenv .venv
56-
$ source .venv/bin/activate
57-
58-
3. Initialize the dev environment for ``simplipy``:
36+
.. code:: python
5937
60-
.. code:: bash
38+
import asyncio
6139
62-
$ script/setup
40+
from aiohttp import ClientSession
41+
import simplipy
6342
64-
4. Run the ``auth`` script:
6543
66-
.. code:: bash
44+
async def main() -> None:
45+
"""Create the aiohttp session and run."""
46+
async with ClientSession() as session:
47+
api = await simplipy.API.async_from_credentials(
48+
"<USERNAME>"
49+
"<PASSWORD>"
50+
session=session,
51+
)
6752
68-
$ script/auth
6953
70-
5. This will open your browser to a SimpliSafe™ login page. Once you log in with your
71-
credentials, you will see a "Verification Pending" webpage (we'll call this
72-
``Tab 1``):
54+
asyncio.run(main())
7355
74-
.. image:: images/ss-login-screen.png
75-
:width: 400
7656
77-
6. You will be prompted to verify your account. Depending on what you have configured in
78-
your SimpliSafe™ account, this could take the form of an email or an SMS:
57+
This will create an object that is in a "pending" state, meaning that it now awaits
58+
two-factor authentication. You can find the type of two-factor authentication by looking
59+
at the ``auth_state`` property:
7960

80-
.. image:: images/ss-verification-email.png
81-
:width: 400
61+
.. code:: python
8262
83-
7. Once you click the "Verify Device" link, a new browser tab (``Tab 2``) will open
84-
and notify you that the verification is successful:
63+
# If your account uses email-based two-factor authentication:
64+
api.auth_state
65+
# >>> simplipy.api.AuthStates.PENDING_2FA_EMAIL
8566
86-
.. image:: images/ss-verification-confirmed.png
87-
:width: 400
67+
# If your account uses SMS-based two-factor authentication:
68+
api.auth_state
69+
# >>> simplipy.api.AuthStates.PENDING_2FA_SMS
8870
89-
8. Return to ``Tab 1``. You need to find an authorization code; the location of this
90-
code will be different depending on which browser you use:
71+
Performing Email-Based Two-Factor Authentication
72+
*************************************************
9173

92-
* Safari: ``Develop -> Show Web Inspector -> Network Tab`` (look for a reference to ``ErrorPage.html``)
93-
* Edge: ``Developer -> Developer Tools -> Console Tab`` (look for a ``Failed to launch`` error)
94-
* Chrome: ``Developer -> Developer Tools -> Console Tab`` (look for a ``Failed to launch`` error)
74+
This type of two-factor authentication requires you to click a link in an email from
75+
SimpliSafe. At any point, you can see if two-factor authentication has been completed:
9576

96-
Look for a reference to a SimpliSafe™ iOS URL (starting with with
97-
``com.simplisafe.mobile``) and note the ``code`` parameter at the very end:
77+
.. code:: python
9878
99-
.. code::
79+
api.async_verify_2fa_email()
10080
101-
com.simplisafe.mobile://auth.simplisafe.com/ios/com.simplisafe.mobile/callback?code=<CODE>
81+
If the two-factor authentication hasn't succeeded yet, ``simplipy`` will raise a
82+
:meth:`Verify2FAPending <simplipy.errors.Verify2FAPending>` exception. If it has
83+
succeeded, the :meth:`API <simplipy.api.API>` object is ready to use.
10284

103-
9. Copy the ``code`` parameter, return to your terminal, and paste it into the prompt.
104-
You should now see this message:
85+
A common pattern in this scenario would be to loop and regularly test for authentication
86+
(eventually timing out as appropriate):
10587

106-
.. code::
88+
.. code:: python
10789
108-
You are now ready to use the SimpliSafe API!
109-
Authorization Code: <CODE>
110-
Code Verifier: <VERIFIER>
90+
try:
91+
async with timeout(30):
92+
try:
93+
await api.async_verify_2fa_email()
94+
except Verify2FAPending as err:
95+
print("Authentication not yet completed")
96+
await asyncio.sleep(3)
97+
except asyncio.TimeoutError as err:
98+
print("Timed out waiting for authentication")
11199
112-
These one-time values are now ready to be used to instantiate an
113-
:meth:`API <simplipy.api.API>` object.
100+
# Ready to use!
114101
115-
Creating an API Object
116-
**********************
102+
Performing SMS-Based Two-Factor Authentication
103+
**********************************************
117104

118-
Once you have an Authorization Code and Code Verifier, you can create an API object like
119-
this:
105+
This type of two-factor authentication requires you to input a code received via SMS.
106+
SimpliSafe. After you receive the code, you use it like this:
120107

121108
.. code:: python
122109
123-
import asyncio
124-
125-
from aiohttp import ClientSession
126-
import simplipy
110+
api.async_verify_2fa_sms("<CODE>")
127111
112+
If the two-factor authentication hasn't succeeded yet, ``simplipy`` will raise a
113+
:meth:`InvalidCredentialsError <simplipy.errors.InvalidCredentialsError>` exception.
114+
If it has succeeded, the :meth:`API <simplipy.api.API>` object is ready to use.
128115

129-
async def main() -> None:
130-
"""Create the aiohttp session and run."""
131-
async with ClientSession() as session:
132-
api = await simplipy.API.async_from_auth(
133-
"<AUTHORIZATION_CODE>",
134-
"<CODE_VERIFIER>",
135-
session=session,
136-
)
137-
138-
# ...
116+
.. code:: python
139117
118+
try:
119+
await api.async_verify_2fa_sms("<CODE>")
120+
except InvalidCredentialsError as err:
121+
print("Invalid SMS 2FA code")
140122
141-
asyncio.run(main())
123+
# Ready to use!
142124
143-
**REMINDER:** this Authorization Code and Code Verifier can only be used once.
125+
Key API Object Properties
126+
*************************
144127

145128
The :meth:`API <simplipy.api.API>` object contains several sensitive properties to be
146129
aware of:

‎examples/test_client_by_auth.py‎

Lines changed: 0 additions & 67 deletions
This file was deleted.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Test system functionality."""
2+
import asyncio
3+
import logging
4+
5+
from aiohttp import ClientSession
6+
from async_timeout import timeout
7+
8+
from simplipy.api import API, AuthStates
9+
from simplipy.errors import SimplipyError, Verify2FAError, Verify2FAPending
10+
11+
_LOGGER = logging.getLogger()
12+
13+
SIMPLISAFE_USERNAME = "<USERNAME>"
14+
SIMPLISAFE_PASSWORD = "<PASSWORD>"
15+
16+
17+
async def main() -> None:
18+
"""Create the aiohttp session and run the example."""
19+
async with ClientSession() as session:
20+
logging.basicConfig(level=logging.INFO)
21+
22+
try:
23+
# Start the process of obtaining an authenticated API object using a
24+
# SimpliSafe username/email and password:
25+
simplisafe = await API.async_from_credentials(
26+
SIMPLISAFE_USERNAME,
27+
SIMPLISAFE_PASSWORD,
28+
session=session,
29+
)
30+
31+
if simplisafe.auth_state == AuthStates.PENDING_2FA_EMAIL:
32+
# If the SimpliSafe account is protected by email-based 2FA, go into a
33+
# loop and periodically see if the user has verified the email
34+
# (eventually timing out if nothing happens):
35+
try:
36+
async with timeout(30):
37+
try:
38+
await simplisafe.async_verify_2fa_email()
39+
except Verify2FAPending as err:
40+
_LOGGER.info(err)
41+
await asyncio.sleep(3)
42+
except asyncio.TimeoutError as err:
43+
raise Verify2FAError(
44+
"Timed out while waiting for email-based 2FA verification"
45+
) from err
46+
elif simplisafe.auth_state == AuthStates.PENDING_2FA_SMS:
47+
# If the SimpliSafe account is protected by SMS-based 2FA, have the user
48+
# input the code they receive:
49+
sms_2fa_code = input("Input your SMS-based 2FA code: ")
50+
await simplisafe.async_verify_2fa_sms(sms_2fa_code)
51+
52+
# If we somehow reach this point without the API object being authenticated,
53+
# halt:
54+
if simplisafe.auth_state != AuthStates.AUTHENTICATED:
55+
raise SimplipyError("API object is not authenticated!")
56+
57+
systems = await simplisafe.async_get_systems()
58+
for system in systems.values():
59+
# Print system state:
60+
_LOGGER.info("System state: %s", system.state)
61+
62+
# Print sensor info:
63+
for serial, sensor in system.sensors.items():
64+
_LOGGER.info(
65+
"Sensor %s: (name: %s, type: %s, triggered: %s)",
66+
serial,
67+
sensor.name,
68+
sensor.type,
69+
sensor.triggered,
70+
)
71+
72+
# Arm/disarm the system:
73+
# await system.async_set_away()
74+
# await system.async_set_home()
75+
# await system.async_set_off()
76+
77+
# Print system events:
78+
events = await system.async_get_events()
79+
for event in events:
80+
_LOGGER.info("Event: %s", event)
81+
82+
# Set PINs:
83+
# await system.async_set_pin("Test PIN", "1235")
84+
# await system.async_remove_pin("Test PIN")
85+
86+
# Interact with locks (if we have them):
87+
for serial, lock in system.locks.items():
88+
_LOGGER.info(
89+
"Lock %s: (name: %s, state: %s)", serial, lock.name, lock.state
90+
)
91+
# await lock.async_lock()
92+
# await lock.async_unlock()
93+
except SimplipyError as err:
94+
_LOGGER.error(err)
95+
96+
97+
asyncio.run(main())

‎script/auth‎

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)