Skip to content
This repository was archived by the owner on Jun 18, 2025. It is now read-only.

Commit f985ce0

Browse files
committed
feat: improve authentication flow based on same JavaScript flow
1 parent 4c6ac2e commit f985ce0

File tree

5 files changed

+119
-13
lines changed

5 files changed

+119
-13
lines changed

custom_components/lifetime_fitness/api.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,47 @@
1212
API_AUTH_REQUEST_PASSWORD_JSON_KEY,
1313
API_AUTH_REQUEST_SUBSCRIPTION_KEY_HEADER,
1414
API_AUTH_REQUEST_SUBSCRIPTION_KEY_HEADER_VALUE,
15-
API_AUTH_STATUS_JSON_KEY,
16-
API_AUTH_STATUS_OK,
1715
API_AUTH_TOKEN_JSON_KEY,
16+
API_AUTH_MESSAGE_JSON_KEY,
17+
API_AUTH_STATUS_JSON_KEY,
1818
API_CLUB_VISITS_ENDPOINT_FORMATSTRING,
1919
API_CLUB_VISITS_ENDPOINT_DATE_FORMAT,
2020
API_CLUB_VISITS_AUTH_HEADER,
21+
AuthenticationResults,
22+
AUTHENTICATION_RESPONSE_MESSAGES,
23+
AUTHENTICATION_RESPONSE_STATUSES
2124
)
2225

2326
_LOGGER = logging.getLogger(__name__)
2427

2528

29+
def handle_authentication_response_json(response_json: dict):
30+
# Based on https://my.lifetime.life/components/login/index.js
31+
message = response_json.get(API_AUTH_MESSAGE_JSON_KEY)
32+
status = response_json.get(API_AUTH_STATUS_JSON_KEY)
33+
if message == AUTHENTICATION_RESPONSE_MESSAGES[AuthenticationResults.SUCCESS]:
34+
return response_json[API_AUTH_TOKEN_JSON_KEY]
35+
elif message == AUTHENTICATION_RESPONSE_MESSAGES[AuthenticationResults.PASSWORD_NEEDS_TO_BE_CHANGED]:
36+
if API_AUTH_TOKEN_JSON_KEY in response_json:
37+
_LOGGER.warning("Life Time password needs to be changed, but API can still be used")
38+
return response_json[API_AUTH_TOKEN_JSON_KEY]
39+
else:
40+
raise ApiPasswordNeedsToBeChanged
41+
elif (
42+
status == AUTHENTICATION_RESPONSE_STATUSES[AuthenticationResults.INVALID] or
43+
message == AUTHENTICATION_RESPONSE_MESSAGES[AuthenticationResults.INVALID]
44+
):
45+
raise ApiInvalidAuth
46+
elif status == AUTHENTICATION_RESPONSE_STATUSES[AuthenticationResults.TOO_MANY_ATTEMPTS]:
47+
raise ApiTooManyAuthenticationAttempts
48+
elif status == AUTHENTICATION_RESPONSE_STATUSES[AuthenticationResults.ACTIVATION_REQUIRED]:
49+
raise ApiActivationRequired
50+
elif status == AUTHENTICATION_RESPONSE_STATUSES[AuthenticationResults.DUPLICATE_EMAIL]:
51+
raise ApiDuplicateEmail
52+
_LOGGER.error("Received unknown authentication error in response: %s", response_json)
53+
raise ApiUnknownAuthError
54+
55+
2656
class Api:
2757
def __init__(self, hass, username: str, password: str) -> None:
2858
self._username = username
@@ -49,18 +79,12 @@ async def authenticate(self):
4979
},
5080
) as response:
5181
response_json = await response.json()
52-
if (
53-
API_AUTH_STATUS_JSON_KEY not in response_json
54-
or response_json[API_AUTH_STATUS_JSON_KEY] != API_AUTH_STATUS_OK
55-
):
56-
_LOGGER.error("Received invalid authentication response: %s", response_json)
57-
raise ApiInvalidAuth
58-
self._sso_token = response_json[API_AUTH_TOKEN_JSON_KEY]
82+
self._sso_token = handle_authentication_response_json(response_json)
5983
except ClientResponseError as err:
6084
if err.status == HTTPStatus.UNAUTHORIZED:
61-
_LOGGER.exception("Received invalid authentication status: %d", err.status)
6285
raise ApiInvalidAuth
63-
raise err
86+
_LOGGER.error("Received unknown status code in authentication response: %d", err.status)
87+
raise ApiUnknownAuthError
6488
except ClientConnectionError:
6589
_LOGGER.exception("Connection error while authenticating to Life Time API")
6690
raise ApiCannotConnect
@@ -106,10 +130,30 @@ class ApiCannotConnect(Exception):
106130
"""Client can't connect to API server"""
107131

108132

133+
class ApiPasswordNeedsToBeChanged(Exception):
134+
"""Password needs to be changed"""
135+
136+
137+
class ApiTooManyAuthenticationAttempts(Exception):
138+
"""There were too many authentication attempts"""
139+
140+
141+
class ApiActivationRequired(Exception):
142+
"""Account activation required"""
143+
144+
145+
class ApiDuplicateEmail(Exception):
146+
"""There are multiple accounts associated with this email"""
147+
148+
109149
class ApiInvalidAuth(Exception):
110150
"""API server returned invalid auth"""
111151

112152

153+
class ApiUnknownAuthError(Exception):
154+
"""API server returned unknown error"""
155+
156+
113157
class ApiAuthRequired(Exception):
114158
"""This API call requires authenticating beforehand"""
115159

custom_components/lifetime_fitness/config_flow.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Config flow for Life Time Fitness integration."""
22
from __future__ import annotations
33

4-
from collections import OrderedDict
54
import logging
65
from typing import Any
76

@@ -96,8 +95,18 @@ async def async_step_user(
9695
info = await validate_input(self.hass, user_input)
9796
except CannotConnect:
9897
errors["base"] = "cannot_connect"
98+
except PasswordNeedsToBeChanged:
99+
errors["base"] = "password_needs_to_be_changed"
100+
except TooManyAuthenticationAttempts:
101+
errors["base"] = "too_many_authentication_attempts"
102+
except ActivationRequired:
103+
errors["base"] = "activation_required"
104+
except DuplicateEmail:
105+
errors["base"] = "duplicate_email"
99106
except InvalidAuth:
100107
errors["base"] = "invalid_auth"
108+
except UnknownAuthError:
109+
errors["base"] = "unknown_auth_error"
101110
except Exception: # pylint: disable=broad-except
102111
_LOGGER.exception("Unexpected exception")
103112
errors["base"] = "unknown"
@@ -118,5 +127,25 @@ class CannotConnect(HomeAssistantError):
118127
"""Error to indicate we cannot connect."""
119128

120129

130+
class PasswordNeedsToBeChanged(HomeAssistantError):
131+
"""Error to indicate there the password needs to be changed."""
132+
133+
134+
class TooManyAuthenticationAttempts(HomeAssistantError):
135+
"""Error to indicate there were too many authentication attempts."""
136+
137+
138+
class ActivationRequired(HomeAssistantError):
139+
"""Error to indicate that account activation is required."""
140+
141+
142+
class DuplicateEmail(HomeAssistantError):
143+
"""Error to indicate there are multiple accounts associated with this email."""
144+
145+
146+
class UnknownAuthError(HomeAssistantError):
147+
"""Error to indicate server returned unexpected authentication error."""
148+
149+
121150
class InvalidAuth(HomeAssistantError):
122151
"""Error to indicate there is invalid auth."""

custom_components/lifetime_fitness/const.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Constants for the Life Time Fitness integration."""
2+
import enum
23

34
DOMAIN = "lifetime_fitness"
45
VERSION = "0.0.0-dev" # Updated by release workflow
@@ -27,8 +28,8 @@
2728
API_AUTH_REQUEST_USERNAME_JSON_KEY = "username"
2829
API_AUTH_REQUEST_PASSWORD_JSON_KEY = "password"
2930
API_AUTH_TOKEN_JSON_KEY = "ssoId"
31+
API_AUTH_MESSAGE_JSON_KEY = "message"
3032
API_AUTH_STATUS_JSON_KEY = "status"
31-
API_AUTH_STATUS_OK = "0"
3233

3334
API_CLUB_VISITS_ENDPOINT_FORMATSTRING = \
3435
"https://myaccount.lifetimefitness.com/myaccount/api/member/clubvisits?end_date={end_date}&start_date={start_date}"
@@ -45,3 +46,25 @@
4546
ATTR_VISITS_THIS_MONTH = "visits_this_month"
4647
ATTR_VISITS_THIS_WEEK = "visits_this_week"
4748
ATTR_LAST_VISIT_TIMESTAMP = "last_visit_timestamp"
49+
50+
51+
class AuthenticationResults(enum.Enum):
52+
SUCCESS = 0
53+
PASSWORD_NEEDS_TO_BE_CHANGED = 1
54+
INVALID = 2
55+
TOO_MANY_ATTEMPTS = 3
56+
ACTIVATION_REQUIRED = 4
57+
DUPLICATE_EMAIL = 5
58+
59+
60+
AUTHENTICATION_RESPONSE_MESSAGES = {
61+
AuthenticationResults.SUCCESS: "Success",
62+
AuthenticationResults.PASSWORD_NEEDS_TO_BE_CHANGED: "Password needs to be changed.",
63+
AuthenticationResults.INVALID: "Invalid username or password"
64+
}
65+
AUTHENTICATION_RESPONSE_STATUSES = {
66+
AuthenticationResults.INVALID: "-201",
67+
AuthenticationResults.TOO_MANY_ATTEMPTS: "-207",
68+
AuthenticationResults.ACTIVATION_REQUIRED: "-208",
69+
AuthenticationResults.DUPLICATE_EMAIL: "-209"
70+
}

custom_components/lifetime_fitness/strings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
},
1313
"error": {
1414
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
15+
"password_needs_to_be_changed": "[%key:common::config_flow::error::password_needs_to_be_changed%]",
16+
"too_many_authentication_attempts": "[%key:common::config_flow::error::too_many_authentication_attempts%]",
17+
"activation_required": "[%key:common::config_flow::error::activation_required%]",
18+
"duplicate_email": "[%key:common::config_flow::error::duplicate_email%]",
1519
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
20+
"unknown_auth_error": "[%key:common::config_flow::error::unknown_auth_error%]",
1621
"unknown": "[%key:common::config_flow::error::unknown%]"
1722
}
1823
},

custom_components/lifetime_fitness/translations/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
"config": {
33
"error": {
44
"cannot_connect": "Failed to connect",
5+
"password_needs_to_be_changed": "Password needs to be changed",
6+
"too_many_authentication_attempts": "Too many attempts, your account has been locked",
7+
"activation_required": "Account activation via email required",
8+
"duplicate_email": "Your email is associated with more than one account. Please login with your username or member #",
59
"invalid_auth": "Invalid authentication",
10+
"unknown_auth_error": "Unexpected error status received from API. Check Home Assistant logs for more details and contact the developer",
611
"unknown": "Unexpected error"
712
},
813
"step": {

0 commit comments

Comments
 (0)