22Auth service for device flow authentication.
33"""
44
5+ from firebase_admin import auth
56import logging
67from typing import Optional , Dict , Any
78from datetime import datetime , timezone , timedelta
1617class AuthConnector :
1718 """
1819 Modal Dict wrapper for device flow authentication.
19-
20+
2021 Stores device codes with expiration (10 minutes) for OAuth device flow.
2122 """
2223
@@ -35,12 +36,12 @@ def __init__(self, device_dict_name: str = DEFAULT_DEVICE_DICT, user_dict_name:
3536
3637 def _is_expired (self , entry : Dict [str , Any ]) -> bool :
3738 """Check if a device code entry is expired."""
38- expires_at = entry .get ("expires_at" )
39+ expires_at = entry .get ("expires_at" )
3940 if expires_at is None :
4041 return False
4142 expires_at = datetime .fromisoformat (expires_at .replace ('Z' , '+00:00' ))
4243 return datetime .now (timezone .utc ) > expires_at
43-
44+
4445 def _delete_session (self , device_code : str , entry : Optional [Dict [str , Any ]]) -> None :
4546 """
4647 Delete both dicts safely if the entry is expired.
@@ -65,7 +66,6 @@ def generate_device_code(self) -> str:
6566
6667 def generate_user_code (self ) -> str :
6768 """Generate a user-friendly code in format ABC-420."""
68-
6969 letters = '' .join (secrets .choice (string .ascii_uppercase ) for _ in range (3 ))
7070 digits = '' .join (secrets .choice (string .digits ) for _ in range (3 ))
7171 return f"{ letters } -{ digits } "
@@ -96,7 +96,7 @@ def create_device_code_entry(
9696 def get_device_code_entry (self , device_code : str ) -> Optional [Dict [str , Any ]]:
9797 """Retrieve device code entry, returns None if not found or expired."""
9898 try :
99-
99+
100100 entry = self .device_store .get (device_code )
101101 if entry is None :
102102 return None
@@ -111,7 +111,7 @@ def get_device_code_entry(self, device_code: str) -> Optional[Dict[str, Any]]:
111111 def get_device_code_by_user_code (self , user_code : str ) -> Optional [str ]:
112112 """
113113 Lookup device_code by user_code.
114-
114+
115115 Returns the device_code if found and not expired, None otherwise.
116116 """
117117 try :
@@ -124,7 +124,7 @@ def get_device_code_by_user_code(self, user_code: str) -> Optional[str]:
124124 if user_code in self .user_store :
125125 del self .user_store [user_code ]
126126 return None
127-
127+
128128 return device_code
129129 except Exception as e :
130130 logger .error (f"Error looking up device_code by user_code: { e } " )
@@ -136,7 +136,7 @@ def update_device_code_status(self, device_code: str, status: str) -> bool:
136136 entry = self .get_device_code_entry (device_code )
137137 if entry is None :
138138 return False
139-
139+
140140 entry ["status" ] = status
141141 self .device_store [device_code ] = entry
142142 logger .info (f"Updated device code { device_code [:8 ]} ... status to: { status } " )
@@ -157,7 +157,7 @@ def set_device_code_authorized(
157157 entry = self .get_device_code_entry (device_code )
158158 if entry is None :
159159 return False
160-
160+
161161 entry ["status" ] = "authorized"
162162 entry ["user_id" ] = user_id
163163 entry ["id_token" ] = id_token
@@ -176,7 +176,7 @@ def set_device_code_denied(self, device_code: str) -> bool:
176176 entry = self .get_device_code_entry (device_code )
177177 if entry is None :
178178 return False
179-
179+
180180 entry ["status" ] = "denied"
181181 entry ["denied_at" ] = datetime .now (timezone .utc ).isoformat ()
182182 self .device_store [device_code ] = entry
@@ -189,31 +189,35 @@ def set_device_code_denied(self, device_code: str) -> bool:
189189 def get_device_code_poll_status (self , device_code : str ) -> Optional [Dict [str , Any ]]:
190190 """
191191 Get device code status for polling endpoint.
192-
192+
193193 Returns status dict with appropriate fields based on state:
194194 - pending: {"status": "pending"}
195195 - authorized: {"status": "authorized", "user_id": ..., "id_token": ..., "refresh_token": ...}
196196 - expired: {"status": "expired", "error": "device_code_expired"}
197197 - denied: {"status": "denied", "error": "user_denied_authorization"}
198198 - not_found: None (treat as expired)
199+
200+ Tokens are deleted after retrieval (one-time use).
199201 """
200202 entry = self .get_device_code_entry (device_code )
201-
203+
202204 if entry is None :
203205 return {
204206 "status" : "expired" ,
205207 "error" : "device_code_expired"
206208 }
207-
209+
208210 status = entry .get ("status" , "pending" )
209-
211+
210212 if status == "authorized" :
211- return {
213+ result = {
212214 "status" : "authorized" ,
213215 "user_id" : entry .get ("user_id" ),
214216 "id_token" : entry .get ("id_token" ),
215217 "refresh_token" : entry .get ("refresh_token" )
216218 }
219+ self ._delete_session (device_code , entry )
220+ return result
217221 elif status == "denied" :
218222 return {
219223 "status" : "denied" ,
@@ -241,3 +245,25 @@ def delete_device_code(self, device_code: str) -> bool:
241245 except Exception as e :
242246 logger .error (f"Error deleting device code: { e } " )
243247 return False
248+
249+ def verify_firebase_token (self , id_token : str ) -> Optional [Dict [str , Any ]]:
250+ """Verify Firebase ID token from website/plugin."""
251+ try :
252+ decoded_token = auth .verify_id_token (id_token )
253+ return {
254+ "user_id" : decoded_token ['uid' ],
255+ "email" : decoded_token .get ('email' ),
256+ "email_verified" : decoded_token .get ('email_verified' , False )
257+ }
258+ except auth .InvalidIdTokenError as e :
259+ logger .error (f"Invalid Firebase token: { e } " )
260+ return None
261+ except auth .ExpiredIdTokenError as e :
262+ logger .error (f"Expired Firebase token: { e } " )
263+ return None
264+ except auth .RevokedIdTokenError as e :
265+ logger .error (f"Revoked Firebase token: { e } " )
266+ return None
267+ except auth .CertificateFetchError as e :
268+ logger .error (f"Firebase certificate fetch error: { e } " )
269+ return None
0 commit comments