Skip to content

Commit e51c715

Browse files
MizterBnkgilley
authored andcommitted
Support 2-step web authentication
1 parent 7a8b7a5 commit e51c715

File tree

1 file changed

+57
-63
lines changed

1 file changed

+57
-63
lines changed

pyecobee/__init__.py

Lines changed: 57 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -173,39 +173,76 @@ def request_tokens(self) -> bool:
173173
except (KeyError, TypeError) as err:
174174
_LOGGER.debug(f"Error obtaining tokens from ecobee: {err}")
175175
return False
176-
176+
177177
def request_tokens_web(self) -> bool:
178-
assert self.auth0_token is not None, "auth0 token must be set before calling request_tokens_web"
178+
# Keep all cookies in a session
179+
session = requests.Session()
179180

180-
resp = requests.get(ECOBEE_AUTH_BASE_URL + "/" + ECOBEE_ENDPOINT_AUTH, cookies={"auth0": self.auth0_token}, params={
181-
"client_id": ECOBEE_WEB_CLIENT_ID,
182-
"scope": "smartWrite",
183-
"response_type": "token",
184-
"response_mode": "form_post",
185-
"redirect_uri": "https://www.ecobee.com/home/authCallback",
186-
"audience": "https://prod.ecobee.com/api/v1",
187-
}, timeout=ECOBEE_DEFAULT_TIMEOUT)
181+
# Get the auth0 token and redirect to the identifier step of the login flow
182+
auth0_url = f"{ECOBEE_AUTH_BASE_URL}/{ECOBEE_ENDPOINT_AUTH}"
183+
resp = session.get(
184+
auth0_url,
185+
params={
186+
"response_type": "token",
187+
"response_mode": "form_post",
188+
"client_id": ECOBEE_WEB_CLIENT_ID,
189+
"redirect_uri": "https://www.ecobee.com/home/authCallback",
190+
"audience": "https://prod.ecobee.com/api/v1",
191+
"scope": "openid smartWrite piiWrite piiRead smartRead deleteGrants",
192+
},
193+
)
194+
if "auth0" not in session.cookies:
195+
_LOGGER.error(
196+
f"Failed to obtain auth0 token from {auth0_url}: {resp.status_code} {resp.text}"
197+
)
198+
return False
199+
else:
200+
self.auth0_token = session.cookies["auth0"]
188201

202+
# Submit the identifier/username and redirect to the password step
203+
identifier_url = resp.url
204+
resp = session.post(
205+
identifier_url,
206+
data={
207+
"username": self.username,
208+
},
209+
)
189210
if resp.status_code != 200:
190-
_LOGGER.error(f"Failed to refresh access token: {resp.status_code} {resp.text}")
211+
_LOGGER.error(f"Failed to submit username: {resp.status_code} {resp.text}")
212+
return False
213+
214+
# Submit the password and get the access_token
215+
password_url = resp.url
216+
resp = session.post(
217+
password_url,
218+
data={
219+
"username": self.username,
220+
"password": self.password,
221+
},
222+
)
223+
if resp.status_code != 200:
224+
_LOGGER.error(f"Failed to submit password: {resp.status_code} {resp.text}")
191225
return False
192-
193-
if (auth0 := resp.cookies.get("auth0")) is None:
194-
_LOGGER.error("Failed to refresh access token: no auth0 cookie in response")
195-
self.auth0_token = auth0
196226

197-
# Parse the response HTML for the access token and expiration
198-
if (access_token := resp.text.split('name="access_token" value="')[1].split('"')[0]) is None:
227+
if (
228+
access_token := resp.text.split('name="access_token" value="')[1].split(
229+
'"'
230+
)[0]
231+
) is None:
199232
_LOGGER.error("Failed to refresh bearer token: no access token in response")
200233
return False
201-
234+
202235
self.access_token = access_token
203236

204-
if (expires_in := resp.text.split('name="expires_in" value="')[1].split('"')[0]) is None:
237+
if (
238+
expires_in := resp.text.split('name="expires_in" value="')[1].split('"')[0]
239+
) is None:
205240
_LOGGER.error("Failed to refresh bearer token: no expiration in response")
206241
return False
207242

208-
expires_at = datetime.datetime.now() + datetime.timedelta(seconds=int(expires_in))
243+
expires_at = datetime.datetime.now() + datetime.timedelta(
244+
seconds=int(expires_in)
245+
)
209246
_LOGGER.debug(f"Access token expires at {expires_at}")
210247

211248
self._write_config()
@@ -214,9 +251,6 @@ def request_tokens_web(self) -> bool:
214251

215252
def refresh_tokens(self) -> bool:
216253
if self.username and self.password:
217-
self.request_auth0_token()
218-
219-
if self.auth0_token is not None:
220254
return self.request_tokens_web()
221255

222256
"""Refreshes ecobee API tokens."""
@@ -246,46 +280,6 @@ def refresh_tokens(self) -> bool:
246280
_LOGGER.debug(f"Error refreshing tokens from ecobee: {err}")
247281
return False
248282

249-
def request_auth0_token(self) -> bool:
250-
"""Get the auth0 token via username/password."""
251-
session = requests.Session()
252-
url = f"{ECOBEE_AUTH_BASE_URL}/{ECOBEE_ENDPOINT_AUTH}"
253-
resp = session.get(
254-
url,
255-
params = {
256-
"response_type": "token",
257-
"response_mode": "form_post",
258-
"client_id": ECOBEE_WEB_CLIENT_ID,
259-
"redirect_uri": "https://www.ecobee.com/home/authCallback",
260-
"audience": "https://prod.ecobee.com/api/v1",
261-
"scope": "openid smartWrite piiWrite piiRead smartRead deleteGrants",
262-
}
263-
)
264-
if resp.status_code != 200:
265-
_LOGGER.error(f"Failed to obtain auth0 token from {url}: {resp.status_code} {resp.text}")
266-
return False
267-
268-
redirect_url = resp.url
269-
resp = session.post(
270-
redirect_url,
271-
data={
272-
"username": self.username,
273-
"password": self.password,
274-
"action": "default"
275-
}
276-
)
277-
if resp.status_code != 200:
278-
_LOGGER.error(f"Failed to obtain auth0 token from {redirect_url}: {resp.status_code} {resp.text}")
279-
return False
280-
if (auth0 := resp.cookies.get("auth0")) is None:
281-
_LOGGER.error(f"Failed to obtain auth0 token from {redirect_url}: no auth0 cookie in response")
282-
self.auth0_token = None
283-
return False
284-
285-
_LOGGER.debug(f"Obtained auth0 token: {auth0}")
286-
self.auth0_token = auth0
287-
return True
288-
289283
def get_thermostats(self) -> bool:
290284
"""Gets a json-list of thermostats from ecobee and caches in self.thermostats."""
291285
param_string = {

0 commit comments

Comments
 (0)