@@ -550,6 +550,29 @@ async def _handle_refresh_response(self, response: httpx2.Response) -> bool:
550550 self ._initialized = False
551551 return False
552552
553+ async def _apply_issuer_binding (self , issuer : str ) -> bool :
554+ """Apply SEP-2352 to the held registration now that the authorization server's issuer is known.
555+
556+ Credentials bound to another issuer are discarded with their tokens so the flow re-registers.
557+ A CIMD record is portable, so it is kept and re-stamped, but tokens it carried over from
558+ another issuer (or of unknown origin, when the record is unstamped) are dropped. Returns
559+ True when the held state was for a different issuer.
560+ """
561+ client_info = self .context .client_info
562+ if client_info is None :
563+ return False
564+ if not credentials_match_issuer (client_info , issuer , self .context .client_metadata_url ):
565+ logger .debug ("Authorization server changed; discarding bound credentials and re-registering" )
566+ self .context .client_info = None
567+ self .context .clear_tokens ()
568+ return True
569+ if client_info .client_id == self .context .client_metadata_url and client_info .issuer != issuer :
570+ self .context .clear_tokens ()
571+ client_info .issuer = issuer
572+ await self .context .storage .set_client_info (client_info )
573+ return True
574+ return False
575+
553576 async def _initialize (self ) -> None :
554577 """Load stored tokens and client info."""
555578 self .context .current_tokens = await self .context .storage .get_tokens ()
@@ -636,35 +659,13 @@ async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx
636659 else :
637660 logger .debug (f"Protected resource metadata discovery failed: { url } " )
638661
639- # SEP-2352: stored credentials are bound to the issuer that registered them.
640- # If the authorization server changed, drop them (and the old tokens) so the
641- # flow re-registers instead of presenting another server's credentials.
642- if (
643- self .context .client_info is not None
644- and self .context .auth_server_url is not None
645- and not credentials_match_issuer (
646- self .context .client_info , self .context .auth_server_url , self .context .client_metadata_url
647- )
662+ # SEP-2352: stored credentials and tokens belong to the issuer they came from.
663+ if self .context .auth_server_url is not None and await self ._apply_issuer_binding (
664+ self .context .auth_server_url
648665 ):
649- logger .debug ("Authorization server changed; discarding bound credentials and re-registering" )
650- self .context .client_info = None
651- self .context .clear_tokens ()
652666 # Any cached AS metadata is for the old server; drop it so a failed
653- # rediscovery cannot leak the old registration/token endpoints into Step 4.
654- self .context .oauth_metadata = None
655- elif (
656- self .context .client_info is not None
657- and self .context .client_info .client_id == self .context .client_metadata_url
658- and self .context .auth_server_url is not None
659- and self .context .client_info .issuer != self .context .auth_server_url
660- ):
661- # A CIMD client_id is portable across authorization servers; the tokens issued
662- # under it and the cached metadata are not. Keep the record, re-stamped. An
663- # unstamped record's tokens have unknown provenance and are dropped the same way.
664- self .context .clear_tokens ()
667+ # rediscovery cannot leak the old endpoints into Steps 4-5.
665668 self .context .oauth_metadata = None
666- self .context .client_info .issuer = self .context .auth_server_url
667- await self .context .storage .set_client_info (self .context .client_info )
668669
669670 asm_discovery_urls = build_oauth_authorization_server_metadata_discovery_urls (
670671 self .context .auth_server_url , self .context .server_url
@@ -688,21 +689,9 @@ async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx
688689 logger .debug (f"OAuth metadata discovery failed: { url } " )
689690
690691 # SEP-2352: on the legacy no-PRM path the issuer is only known after ASM
691- # discovery, so re-evaluate the binding here using the discovered metadata
692- # issuer (mirroring the bound_issuer fallback in Step 4).
693- if (
694- self .context .client_info is not None
695- and self .context .auth_server_url is None
696- and self .context .oauth_metadata is not None
697- and not credentials_match_issuer (
698- self .context .client_info ,
699- str (self .context .oauth_metadata .issuer ),
700- self .context .client_metadata_url ,
701- )
702- ):
703- logger .debug ("Authorization server changed; discarding bound credentials and re-registering" )
704- self .context .client_info = None
705- self .context .clear_tokens ()
692+ # discovery (mirroring the bound_issuer fallback in Step 4).
693+ if self .context .auth_server_url is None and self .context .oauth_metadata is not None :
694+ await self ._apply_issuer_binding (str (self .context .oauth_metadata .issuer ))
706695
707696 # Step 3: Apply scope selection strategy
708697 self .context .client_metadata .scope = get_client_metadata_scopes (
0 commit comments