11"""Login handler for blink."""
22
33import time
4+ import uuid
45import logging
56from aiohttp import (
67 ClientSession ,
1011)
1112from blinkpy import api
1213from blinkpy .helpers import util
14+ from blinkpy .helpers .pkce import generate_pkce_pair
1315from blinkpy .helpers .constants import (
1416 BLINK_URL ,
1517 APP_BUILD ,
@@ -67,6 +69,11 @@ def __init__(
6769 # Callback to notify on token refresh
6870 self .callback = callback
6971
72+ # OAuth v2 attributes
73+ self .hardware_id = login_data .get ("hardware_id" )
74+ if not self .hardware_id :
75+ self .hardware_id = str (uuid .uuid4 ()).upper ()
76+
7077 @property
7178 def login_attributes (self ):
7279 """Return a dictionary of login attributes."""
@@ -79,6 +86,7 @@ def login_attributes(self):
7986 self .data ["client_id" ] = self .client_id
8087 self .data ["account_id" ] = self .account_id
8188 self .data ["user_id" ] = self .user_id
89+ self .data ["hardware_id" ] = self .hardware_id
8290 return self .data
8391
8492 @property
@@ -176,8 +184,27 @@ def extract_tier_info(self):
176184 async def startup (self ):
177185 """Initialize tokens for communication."""
178186 self .validate_login ()
179- if None in self .login_attributes .values ():
180- await self .refresh_tokens ()
187+
188+ if self .refresh_token and self .hardware_id :
189+ _LOGGER .debug ("Attempting OAuth v2 token refresh" )
190+ try :
191+ token_data = await api .oauth_refresh_token (
192+ self , self .refresh_token , self .hardware_id
193+ )
194+ if token_data :
195+ await self ._process_token_data (token_data )
196+ _LOGGER .info ("OAuth v2 token refresh successful" )
197+ return
198+ except Exception as error :
199+ _LOGGER .debug ("OAuth v2 refresh failed: %s" , error )
200+
201+ _LOGGER .debug ("Attempting OAuth v2 login flow" )
202+ success = await self ._oauth_login_flow ()
203+ if success :
204+ _LOGGER .info ("OAuth v2 login successful" )
205+ return
206+
207+ raise LoginError ("OAuth v2 login failed" )
181208
182209 async def validate_response (self , response : ClientResponse , json_resp ):
183210 """Check for valid response."""
@@ -271,6 +298,136 @@ async def query(
271298 )
272299 return None
273300
301+ async def _oauth_login_flow (self ):
302+ """
303+ Execute complete OAuth 2.0 login flow with PKCE.
304+
305+ Returns:
306+ bool: True if successful
307+
308+ """
309+ # Step 1: Generate PKCE
310+ code_verifier , code_challenge = generate_pkce_pair ()
311+
312+ # Step 2: Authorization request
313+ auth_success = await api .oauth_authorize_request (
314+ self , self .hardware_id , code_challenge
315+ )
316+ if not auth_success :
317+ _LOGGER .error ("OAuth authorization request failed" )
318+ return False
319+
320+ # Step 3: Get CSRF token
321+ csrf_token = await api .oauth_get_signin_page (self )
322+ if not csrf_token :
323+ _LOGGER .error ("Failed to get CSRF token" )
324+ return False
325+
326+ # Step 4: Login
327+ email = self .data .get ("username" )
328+ password = self .data .get ("password" )
329+
330+ login_result = await api .oauth_signin (self , email , password , csrf_token )
331+
332+ # Step 4b: Handle 2FA if needed
333+ if login_result == "2FA_REQUIRED" :
334+ # Store CSRF token and verifier for later use
335+ self ._oauth_csrf_token = csrf_token
336+ self ._oauth_code_verifier = code_verifier
337+ # Raise exception to let the app handle 2FA prompt
338+ _LOGGER .info ("Two-factor authentication required." )
339+ raise BlinkTwoFARequiredError
340+ elif login_result != "SUCCESS" :
341+ _LOGGER .error ("Login failed" )
342+ return False
343+
344+ # Step 5: Get authorization code
345+ code = await api .oauth_get_authorization_code (self )
346+ if not code :
347+ _LOGGER .error ("Failed to get authorization code" )
348+ return False
349+
350+ # Step 6: Exchange code for token
351+ token_data = await api .oauth_exchange_code_for_token (
352+ self , code , code_verifier , self .hardware_id
353+ )
354+
355+ if not token_data :
356+ _LOGGER .error ("Failed to exchange code for token" )
357+ return False
358+
359+ # Process tokens
360+ await self ._process_token_data (token_data )
361+ return True
362+
363+ async def _process_token_data (self , token_data ):
364+ """Process token response data."""
365+ self .token = token_data .get ("access_token" )
366+ self .refresh_token = token_data .get ("refresh_token" )
367+
368+ # Set expiration
369+ expires_in = token_data .get ("expires_in" , 3600 )
370+ self .expires_in = expires_in
371+ self .expiration_date = time .time () + expires_in
372+
373+ # Get tier info if needed (for account_id, region_id, host)
374+ if not self .host or not self .region_id or not self .account_id :
375+ try :
376+ self .tier_info = await self .get_tier_info ()
377+ self .extract_tier_info ()
378+ except Exception as error :
379+ _LOGGER .warning ("Failed to get tier info: %s" , error )
380+
381+ async def complete_2fa_login (self , twofa_code ):
382+ """
383+ Complete OAuth v2 login after 2FA verification.
384+
385+ Args:
386+ twofa_code: 2FA code from user
387+
388+ Returns:
389+ bool: True if successful
390+
391+ """
392+ # Check if we have stored OAuth state
393+ if not hasattr (self , "_oauth_csrf_token" ) or not hasattr (
394+ self , "_oauth_code_verifier"
395+ ):
396+ _LOGGER .error ("No OAuth 2FA state found. Start login flow first." )
397+ return False
398+
399+ csrf_token = self ._oauth_csrf_token
400+ code_verifier = self ._oauth_code_verifier
401+
402+ # Verify 2FA
403+ if not await api .oauth_verify_2fa (self , csrf_token , twofa_code ):
404+ _LOGGER .error ("2FA verification failed" )
405+ return False
406+
407+ # Step 5: Get authorization code
408+ code = await api .oauth_get_authorization_code (self )
409+ if not code :
410+ _LOGGER .error ("Failed to get authorization code after 2FA" )
411+ return False
412+
413+ # Step 6: Exchange code for token
414+ token_data = await api .oauth_exchange_code_for_token (
415+ self , code , code_verifier , self .hardware_id
416+ )
417+
418+ if not token_data :
419+ _LOGGER .error ("Failed to exchange code for token after 2FA" )
420+ return False
421+
422+ # Process tokens
423+ await self ._process_token_data (token_data )
424+
425+ # Clean up temporary state
426+ delattr (self , "_oauth_csrf_token" )
427+ delattr (self , "_oauth_code_verifier" )
428+
429+ return True
430+
274431
275432class TokenRefreshFailed (Exception ):
276433 """Class to throw failed refresh exception."""
0 commit comments