3131 AuthenticationError ,
3232 TokenError ,
3333 ValidationError ,
34+ generate_pkce_pair ,
35+ generate_state ,
36+ build_authorization_url
3437)
3538
3639logger = logging .getLogger (__name__ )
@@ -43,16 +46,6 @@ class AgentConfig:
4346 agent_id : str
4447 agent_secret : str
4548
46-
47- def generate_state () -> str :
48- """Generate a secure random state parameter."""
49- return base64 .urlsafe_b64encode (os .urandom (16 )).decode ('utf-8' ).rstrip ('=' )
50-
51-
52- def build_authorization_url (base_url : str , params : Dict [str , Any ]) -> str :
53- """Build authorization URL with parameters."""
54- return f"{ base_url } ?{ urlencode (params )} "
55-
5649class AgentAuthManager :
5750 """Agent-enhanced OAuth2 authentication manager for AI agents."""
5851
@@ -91,7 +84,12 @@ async def get_agent_token(self, scopes: Optional[List[str]] = None) -> OAuthToke
9184 self .config .scope = ' ' .join (scopes )
9285
9386 # Start authentication flow
94- init_response = await native_client .authenticate ()
87+ code_verifier , code_challenge = generate_pkce_pair ()
88+ params = {
89+ "code_challenge" : code_challenge ,
90+ "code_challenge_method" : "S256" ,
91+ }
92+ init_response = await native_client .authenticate (params = params )
9593
9694 if native_client .flow_status == FlowStatus .SUCCESS_COMPLETED :
9795 auth_data = init_response .get ('authData' , {})
@@ -127,7 +125,7 @@ async def get_agent_token(self, scopes: Optional[List[str]] = None) -> OAuthToke
127125 raise TokenError ("No authorization code received from authentication flow." )
128126
129127 # Exchange code for token
130- token = await self .token_client .get_token ('authorization_code' , code = code )
128+ token = await self .token_client .get_token ('authorization_code' , code = code , code_verifier = code_verifier )
131129
132130 # Restore original scope
133131 if scopes :
@@ -180,12 +178,57 @@ def get_authorization_url(
180178 auth_params
181179 )
182180 return auth_url , state
181+
182+ def get_authorization_url_with_pkce (
183+ self ,
184+ scopes : List [str ],
185+ state : Optional [str ] = None ,
186+ resource : Optional [str ] = None ,
187+ ** kwargs : Any ,
188+ ) -> Tuple [str , str , str ]:
189+ """Generate authorization URL for user authentication.
190+
191+ :param scopes: List of OAuth scopes to request
192+ :param state: Optional state parameter (generated if not provided)
193+ :param resource: Optional resource parameter
194+ :param kwargs: Additional parameters for the authorization URL
195+ :return: Tuple of (authorization_url, state)
196+ """
197+ if not state :
198+ state = generate_state ()
199+
200+ code_verifier , code_challenge = generate_pkce_pair ()
201+
202+ auth_params = {
203+ "client_id" : self .config .client_id ,
204+ "redirect_uri" : self .config .redirect_uri ,
205+ "scope" : " " .join (scopes ),
206+ "state" : state ,
207+ "response_type" : "code" ,
208+ "code_challenge" : code_challenge ,
209+ "code_challenge_method" : "S256" ,
210+ }
211+
212+ if resource :
213+ auth_params ["resource" ] = resource
214+
215+ if self .agent_config :
216+ auth_params ["requested_actor" ] = self .agent_config .agent_id
217+
218+ auth_params .update (kwargs )
219+
220+ auth_url = build_authorization_url (
221+ f"{ self .config .base_url } /oauth2/authorize" ,
222+ auth_params
223+ )
224+ return auth_url , state , code_verifier
183225
184226 async def get_obo_token (
185227 self ,
186228 auth_code : str ,
187229 agent_token : str ,
188230 scopes : Optional [List [str ]] = None ,
231+ code_verifier : Optional [str ] = None
189232 ) -> OAuthToken :
190233 """Get on-behalf-of (OBO) token for user using authorization code.
191234
@@ -206,6 +249,7 @@ async def get_obo_token(
206249 code = auth_code ,
207250 scope = scope_str ,
208251 actor_token = actor_token_val ,
252+ code_verifier = code_verifier
209253 )
210254 return token
211255
0 commit comments