@@ -32,6 +32,7 @@ def __init__(
3232 session = None ,
3333 agent = DEFAULT_USER_AGENT ,
3434 app_build = APP_BUILD ,
35+ callback = None ,
3536 ):
3637 """
3738 Initialize auth handler.
@@ -48,8 +49,8 @@ def __init__(
4849 self .data = login_data
4950 self .token = login_data .get ("token" , None )
5051 self .expires_in = login_data .get ("expires_in" , None )
51- self .expiration_date = None
52- self ._refresh_token = login_data .get ("refresh_token" , None )
52+ self .expiration_date = login_data . get ( "expiration_date" , None )
53+ self .refresh_token = login_data .get ("refresh_token" , None )
5354 self .host = login_data .get ("host" , None )
5455 self .region_id = login_data .get ("region_id" , None )
5556 self .client_id = login_data .get ("client_id" , None )
@@ -63,12 +64,16 @@ def __init__(
6364 self ._app_build = app_build
6465 self .session = session if session else ClientSession ()
6566
67+ # Callback to notify on token refresh
68+ self .callback = callback
69+
6670 @property
6771 def login_attributes (self ):
6872 """Return a dictionary of login attributes."""
6973 self .data ["token" ] = self .token
7074 self .data ["expires_in" ] = self .expires_in
71- self .data ["refresh_token" ] = self ._refresh_token
75+ self .data ["expiration_date" ] = self .expiration_date
76+ self .data ["refresh_token" ] = self .refresh_token
7277 self .data ["host" ] = self .host
7378 self .data ["region_id" ] = self .region_id
7479 self .data ["client_id" ] = self .client_id
@@ -96,12 +101,6 @@ def validate_login(self):
96101 self .data = util .prompt_login_data (self .data )
97102 self .data = util .validate_login_data (self .data )
98103
99- def validate_2fa (self ):
100- """Check 2FA information and prompt if not available."""
101- self .data ["2fa_code" ] = self .data .get ("2fa_code" , None )
102- if not self .no_prompt :
103- self .data = util .prompt_2fa_data (self .data )
104-
105104 async def login (self , login_url = LOGIN_ENDPOINT , refresh = False ):
106105 """Attempt OAuth login to blink servers."""
107106 self .validate_login ()
@@ -116,8 +115,7 @@ async def login(self, login_url=LOGIN_ENDPOINT, refresh=False):
116115 if response .status == 200 :
117116 return await response .json ()
118117 if response .status == 412 :
119- self .validate_2fa ()
120- return await self .login ()
118+ raise BlinkTwoFARequiredError
121119 raise LoginError
122120 except AttributeError as error :
123121 raise LoginError from error
@@ -130,16 +128,24 @@ def logout(self, blink):
130128 """Log out."""
131129 return api .request_logout (blink )
132130
133- async def refresh_token (self ):
134- """Refresh auth token."""
131+ async def refresh_tokens (self , refresh = False ):
132+ """Create or refresh access token."""
135133 self .is_errored = True
136134 try :
137- _LOGGER .info ("Token expired, attempting automatic refresh." )
138- self .login_response = await self .login ()
135+ _LOGGER .info (
136+ f"{ 'Refreshing' if refresh else 'Obtaining' } authentication token."
137+ )
138+ self .login_response = await self .login (refresh = refresh )
139139 self .extract_login_info ()
140- self .tier_info = await self .get_tier_info ()
141- self .extract_tier_info ()
140+
141+ if not refresh :
142+ self .tier_info = await self .get_tier_info ()
143+ self .extract_tier_info ()
144+
142145 self .is_errored = False
146+ except BlinkTwoFARequiredError as error :
147+ _LOGGER .error ("Two-factor authentication required. Waiting for otp." )
148+ raise BlinkTwoFARequiredError from error
143149 except LoginError as error :
144150 _LOGGER .error ("Login endpoint failed. Try again later." )
145151 raise TokenRefreshFailed from error
@@ -153,7 +159,7 @@ def extract_login_info(self):
153159 self .token = self .login_response ["access_token" ]
154160 self .expires_in = self .login_response ["expires_in" ]
155161 self .expiration_date = time .time () + self .expires_in
156- self ._refresh_token = self .login_response ["refresh_token" ]
162+ self .refresh_token = self .login_response ["refresh_token" ]
157163
158164 def extract_tier_info (self ):
159165 """Extract tier info from tier info response."""
@@ -165,7 +171,7 @@ async def startup(self):
165171 """Initialize tokens for communication."""
166172 self .validate_login ()
167173 if None in self .login_attributes .values ():
168- await self .refresh_token ()
174+ await self .refresh_tokens ()
169175
170176 async def validate_response (self , response : ClientResponse , json_resp ):
171177 """Check for valid response."""
@@ -191,7 +197,7 @@ async def validate_response(self, response: ClientResponse, json_resp):
191197 def need_refresh (self ):
192198 """Check if token needs refresh."""
193199 if self .expiration_date is None :
194- return self ._refresh_token is not None
200+ return self .refresh_token is not None
195201
196202 return self .expiration_date - time .time () < 60
197203
@@ -219,7 +225,14 @@ async def query(
219225 """
220226 try :
221227 if not skip_refresh_check and self .need_refresh ():
222- await self .login (refresh = True )
228+ await self .refresh_tokens (refresh = True )
229+
230+ if "Authorization" in headers :
231+ # update the authorization header with the new token
232+ headers ["Authorization" ] = f"Bearer { self .token } "
233+
234+ if self .callback is not None :
235+ self .callback ()
223236
224237 if reqtype == "get" :
225238 response = await self .session .get (
@@ -253,7 +266,7 @@ async def query(
253266 except UnauthorizedError :
254267 try :
255268 if not is_retry :
256- await self .refresh_token ()
269+ await self .refresh_tokens ()
257270 return await self .query (
258271 url = url ,
259272 data = data ,
@@ -282,5 +295,9 @@ class BlinkBadResponse(Exception):
282295 """Class to throw bad json response exception."""
283296
284297
298+ class BlinkTwoFARequiredError (Exception ):
299+ """Class to throw two-factor authentication required exception."""
300+
301+
285302class UnauthorizedError (Exception ):
286303 """Class to throw an unauthorized access error."""
0 commit comments