4242ENV_GOOGLE_CLIENT_ID = "GOOGLE_CLIENT_ID"
4343ENV_GOOGLE_CLIENT_SECRET = "GOOGLE_CLIENT_SECRET" # noqa: S105
4444
45- GOOGLE_DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code "
46- GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" # noqa: S105
45+ ENV_GOOGLE_OAUTH_BASE_URL = "GOOGLE_OAUTH_BASE_URL "
46+ DEFAULT_GOOGLE_OAUTH_BASE_URL = "https://oauth2.googleapis.com"
4747DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
4848
4949CAPABILITY_SCOPES : dict [str , str ] = {
@@ -324,6 +324,19 @@ def _require_namespaced_capability(capability_id: Any) -> str:
324324 return capability
325325
326326
327+ def _google_oauth_base_url () -> str :
328+ value = _optional_text (os .environ .get (ENV_GOOGLE_OAUTH_BASE_URL ))
329+ return value or DEFAULT_GOOGLE_OAUTH_BASE_URL
330+
331+
332+ def _google_device_code_url () -> str :
333+ return f"{ _google_oauth_base_url ()} /device/code"
334+
335+
336+ def _google_token_url () -> str :
337+ return f"{ _google_oauth_base_url ()} /token"
338+
339+
327340def _google_client_id () -> str :
328341 value = _optional_text (os .environ .get (ENV_GOOGLE_CLIENT_ID ))
329342 if not value :
@@ -434,73 +447,45 @@ def _handle_auth_begin(params: dict[str, Any]) -> dict[str, Any]:
434447 flow_id = f"gaf_{ secrets .token_hex (12 )} "
435448 expires_epoch = now_epoch + _auth_flow_ttl_seconds ()
436449
437- # Use device code flow when Google OAuth credentials are configured,
438- # otherwise fall back to legacy authorization_code placeholder flow.
439- client_id = _optional_text (os .environ .get (ENV_GOOGLE_CLIENT_ID ))
440- client_secret = _optional_text (os .environ .get (ENV_GOOGLE_CLIENT_SECRET ))
450+ # Require Google OAuth credentials — fail loudly when not configured.
451+ client_id = _google_client_id ()
452+ _google_client_secret () # validate presence early
441453 scope = CAPABILITY_SCOPES .get (capability_id )
442-
443- if client_id and client_secret and scope :
444- # Device code flow (RFC 8628)
445- device_resp = _http_post_form (
446- GOOGLE_DEVICE_CODE_URL ,
447- {"client_id" : client_id , "scope" : scope },
454+ if not scope :
455+ raise BridgeError (
456+ "capability_invalid_input" ,
457+ f"no OAuth scopes configured for { capability_id } " ,
448458 )
449- device_error = _optional_text (device_resp .get ("error" ))
450- if device_error :
451- error_desc = (
452- _optional_text (device_resp .get ("error_description" )) or device_error
453- )
454- raise BridgeError (
455- "capability_backend_unavailable" ,
456- f"Google device code request failed: { error_desc } " ,
457- )
458-
459- device_code = _optional_text (device_resp .get ("device_code" ))
460- user_code = _optional_text (device_resp .get ("user_code" ))
461- verification_url = _optional_text (device_resp .get ("verification_url" ))
462- google_interval = _int_claim (device_resp , "interval" ) or 5
463- google_expires_in = _int_claim (device_resp , "expires_in" )
464-
465- if not device_code or not user_code or not verification_url :
466- raise BridgeError (
467- "capability_backend_unavailable" ,
468- "Google device code response missing required fields" ,
469- )
470459
471- if google_expires_in and google_expires_in < (expires_epoch - now_epoch ):
472- expires_epoch = now_epoch + google_expires_in
460+ # Device code flow (RFC 8628)
461+ device_resp = _http_post_form (
462+ _google_device_code_url (),
463+ {"client_id" : client_id , "scope" : scope },
464+ )
465+ device_error = _optional_text (device_resp .get ("error" ))
466+ if device_error :
467+ error_desc = (
468+ _optional_text (device_resp .get ("error_description" )) or device_error
469+ )
470+ raise BridgeError (
471+ "capability_backend_unavailable" ,
472+ f"Google device code request failed: { error_desc } " ,
473+ )
473474
474- state = _read_state ()
475- _prune_expired_flows (state = state , now_epoch = now_epoch )
476- state ["auth_flows" ][flow_id ] = {
477- "user_id" : claims .user_id ,
478- "capability_id" : capability_id ,
479- "account_hint" : account_hint ,
480- "nonce" : nonce ,
481- "issued_at" : now_epoch ,
482- "expires_at" : expires_epoch ,
483- "device_code" : device_code ,
484- "poll_interval" : google_interval ,
485- }
486- _write_state (state )
475+ device_code = _optional_text (device_resp .get ("device_code" ))
476+ user_code = _optional_text (device_resp .get ("user_code" ))
477+ verification_url = _optional_text (device_resp .get ("verification_url" ))
478+ google_interval = _int_claim (device_resp , "interval" ) or 5
479+ google_expires_in = _int_claim (device_resp , "expires_in" )
487480
488- flow_state = {
489- "flow_id" : flow_id ,
490- "nonce" : nonce ,
491- "device_code" : device_code ,
492- }
493- return {
494- "auth_url" : verification_url ,
495- "expires_at" : _iso8601_utc (expires_epoch ),
496- "flow_state" : flow_state ,
497- "flow_type" : "device_code" ,
498- "user_code" : user_code ,
499- "poll_interval_seconds" : google_interval ,
500- }
481+ if not device_code or not user_code or not verification_url :
482+ raise BridgeError (
483+ "capability_backend_unavailable" ,
484+ "Google device code response missing required fields" ,
485+ )
501486
502- # Legacy authorization_code placeholder flow (no Google credentials configured)
503- from urllib . parse import quote_plus
487+ if google_expires_in and google_expires_in < ( expires_epoch - now_epoch ):
488+ expires_epoch = now_epoch + google_expires_in
504489
505490 state = _read_state ()
506491 _prune_expired_flows (state = state , now_epoch = now_epoch )
@@ -511,23 +496,23 @@ def _handle_auth_begin(params: dict[str, Any]) -> dict[str, Any]:
511496 "nonce" : nonce ,
512497 "issued_at" : now_epoch ,
513498 "expires_at" : expires_epoch ,
499+ "device_code" : device_code ,
500+ "poll_interval" : google_interval ,
514501 }
515502 _write_state (state )
503+
516504 flow_state = {
517505 "flow_id" : flow_id ,
518506 "nonce" : nonce ,
507+ "device_code" : device_code ,
519508 }
520- auth_url = (
521- "https://auth.gog.local/authorize"
522- f"?capability={ quote_plus (capability_id )} "
523- f"&account={ quote_plus (account_hint )} "
524- f"&flow_id={ quote_plus (flow_id )} "
525- f"&nonce={ quote_plus (nonce )} "
526- )
527509 return {
528- "auth_url" : auth_url ,
510+ "auth_url" : verification_url ,
529511 "expires_at" : _iso8601_utc (expires_epoch ),
530512 "flow_state" : flow_state ,
513+ "flow_type" : "device_code" ,
514+ "user_code" : user_code ,
515+ "poll_interval_seconds" : google_interval ,
531516 }
532517
533518
@@ -589,7 +574,7 @@ def _handle_auth_poll(params: dict[str, Any]) -> dict[str, Any]:
589574 client_secret = _google_client_secret ()
590575
591576 token_resp = _http_post_form (
592- GOOGLE_TOKEN_URL ,
577+ _google_token_url () ,
593578 {
594579 "client_id" : client_id ,
595580 "client_secret" : client_secret ,
@@ -897,7 +882,7 @@ def _refresh_token_if_needed(*, account_key: str, vault_ref: str | None) -> None
897882 except BridgeError :
898883 return
899884 token_resp = _http_post_form (
900- GOOGLE_TOKEN_URL ,
885+ _google_token_url () ,
901886 {
902887 "client_id" : client_id ,
903888 "client_secret" : client_secret ,
0 commit comments