@@ -27,8 +27,8 @@ internal class AuthenticationHandler : DelegatingHandler
2727 private const string BearerAuthenticationScheme = "Bearer" ;
2828 private const string PopAuthenticationScheme = "Pop" ;
2929 private int MaxRetry { get ; set ; } = 1 ;
30- private PopTokenRequestContext popTokenRequestContext ;
31- private Request popRequest = GraphSession . Instance . GraphRequestPopContext . PopPipeline . CreateRequest ( ) ;
30+ private TokenRequestContext popTokenRequestContext ;
31+ private string cachedNonce ;
3232
3333 public AzureIdentityAccessTokenProvider AuthenticationProvider { get ; set ; }
3434
@@ -52,10 +52,22 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5252
5353 HttpResponseMessage response = await base . SendAsync ( httpRequestMessage , cancellationToken ) . ConfigureAwait ( false ) ;
5454
55- // Continuous nonce extraction on each request
56- if ( GraphSession . Instance . GraphOption . EnableATPoPForMSGraph )
55+ // Extract nonce from API responses for future PoP requests
56+ if ( GraphSession . Instance . GraphOption . EnableATPoPForMSGraph && IsApiRequest ( httpRequestMessage . RequestUri ) )
5757 {
58- popTokenRequestContext = new PopTokenRequestContext ( GraphSession . Instance . AuthContext . Scopes , isProofOfPossessionEnabled : true , proofOfPossessionNonce : WwwAuthenticateParameters . CreateFromAuthenticationHeaders ( response . Headers , PopAuthenticationScheme ) . Nonce , request : popRequest ) ;
58+ try
59+ {
60+ var wwwAuthParams = WwwAuthenticateParameters . CreateFromAuthenticationHeaders ( response . Headers , PopAuthenticationScheme ) ;
61+ if ( wwwAuthParams ? . Nonce != null && ! string . IsNullOrEmpty ( wwwAuthParams . Nonce ) )
62+ {
63+ cachedNonce = wwwAuthParams . Nonce ;
64+ }
65+ }
66+ catch ( Exception ex )
67+ {
68+ System . Diagnostics . Debug . WriteLine ( $ "AuthenticationHandler: Failed to extract PoP nonce: { ex . Message } ") ;
69+ // Don't throw - nonce extraction failure shouldn't break the response
70+ }
5971 }
6072
6173 // Check if response is a 401 & is not a streamed body (is buffered)
@@ -76,17 +88,48 @@ private async Task AuthenticateRequestAsync(HttpRequestMessage httpRequestMessag
7688 {
7789 if ( AuthenticationProvider != null )
7890 {
91+ // Determine if this is an API request that should use PoP (when enabled)
92+ // vs an authentication request that should always use Bearer
93+ bool isApiRequest = IsApiRequest ( httpRequestMessage . RequestUri ) ;
94+ bool shouldUsePoP = GraphSession . Instance . GraphOption . EnableATPoPForMSGraph && isApiRequest ;
95+
96+ // Debug logging for flow routing
7997 if ( GraphSession . Instance . GraphOption . EnableATPoPForMSGraph )
8098 {
81- popRequest . Method = RequestMethod . Parse ( httpRequestMessage . Method . Method . ToUpper ( ) ) ;
82- popRequest . Uri . Reset ( httpRequestMessage . RequestUri ) ;
83- foreach ( var header in httpRequestMessage . Headers )
99+ var requestType = isApiRequest ? "API" : "Auth" ;
100+ var tokenType = shouldUsePoP ? "PoP" : "Bearer" ;
101+ System . Diagnostics . Debug . WriteLine ( $ "AuthenticationHandler: { requestType } request to { httpRequestMessage . RequestUri ? . Host } using { tokenType } token") ;
102+ }
103+
104+ if ( shouldUsePoP )
105+ {
106+ // API Request with PoP enabled - use PoP tokens ONLY
107+ try
108+ {
109+ // Create proper TokenRequestContext for PoP
110+ // Note: cachedNonce may be null for initial requests - this is expected
111+ popTokenRequestContext = new TokenRequestContext (
112+ scopes : GraphSession . Instance . AuthContext . Scopes ,
113+ parentRequestId : null ,
114+ claims : additionalAuthenticationContext ? . ContainsKey ( ClaimsKey ) == true ? additionalAuthenticationContext [ ClaimsKey ] ? . ToString ( ) : null ,
115+ tenantId : null ,
116+ isCaeEnabled : false ,
117+ isProofOfPossessionEnabled : true ,
118+ proofOfPossessionNonce : cachedNonce // May be null for initial requests
119+ ) ;
120+
121+ // Get TokenCredential from existing AuthenticationProvider
122+ var tokenCredential = await AuthenticationHelpers . GetTokenCredentialAsync (
123+ GraphSession . Instance . AuthContext , cancellationToken ) . ConfigureAwait ( false ) ;
124+
125+ var accessToken = await tokenCredential . GetTokenAsync ( popTokenRequestContext , cancellationToken ) . ConfigureAwait ( false ) ;
126+ httpRequestMessage . Headers . Authorization = new AuthenticationHeaderValue ( PopAuthenticationScheme , accessToken . Token ) ;
127+ }
128+ catch ( Exception ex ) when ( ! ( ex is OperationCanceledException ) )
84129 {
85- popRequest . Headers . Add ( header . Key , header . Value . First ( ) ) ;
130+ // Re-throw with context for PoP-specific failures
131+ throw new AuthenticationException ( $ "Failed to acquire PoP token for { httpRequestMessage . RequestUri } : { ex . Message } ", ex ) ;
86132 }
87-
88- var accessToken = await GraphSession . Instance . GraphRequestPopContext . PopInteractiveBrowserCredential . GetTokenAsync ( popTokenRequestContext , cancellationToken ) . ConfigureAwait ( false ) ;
89- httpRequestMessage . Headers . Authorization = new AuthenticationHeaderValue ( PopAuthenticationScheme , accessToken . Token ) ;
90133 }
91134 else
92135 {
@@ -156,5 +199,52 @@ private static async Task DrainAsync(HttpResponseMessage response)
156199 }
157200 response . Dispose ( ) ;
158201 }
202+
203+ /// <summary>
204+ /// Determines if the request is an API request that should use PoP when enabled,
205+ /// vs an authentication/token request that should always use Bearer tokens.
206+ /// This method implements the core routing logic for AT-PoP:
207+ /// - Graph API endpoints → PoP tokens (when enabled)
208+ /// - Authentication endpoints → Bearer tokens (always)
209+ /// - Unknown endpoints → Bearer tokens (safe default)
210+ /// </summary>
211+ /// <param name="requestUri">The request URI to evaluate</param>
212+ /// <returns>True if this is an API request, false if it's an authentication request</returns>
213+ private static bool IsApiRequest ( Uri requestUri )
214+ {
215+ if ( requestUri == null ) return false ;
216+
217+ var host = requestUri . Host ? . ToLowerInvariant ( ) ;
218+ var path = requestUri . AbsolutePath ? . ToLowerInvariant ( ) ;
219+
220+ // Microsoft Graph API endpoints that should use PoP
221+ if ( host ? . Contains ( "graph.microsoft.com" ) == true ||
222+ host ? . Contains ( "graph.microsoft.us" ) == true ||
223+ host ? . Contains ( "microsoftgraph.chinacloudapi.cn" ) == true ||
224+ host ? . Contains ( "graph.microsoft.de" ) == true )
225+ {
226+ // Exclude authentication/token endpoints - these should always use Bearer
227+ if ( path ? . Contains ( "/oauth2/" ) == true ||
228+ path ? . Contains ( "/token" ) == true ||
229+ path ? . Contains ( "/authorize" ) == true ||
230+ path ? . Contains ( "/devicecode" ) == true )
231+ {
232+ return false ; // Authentication request
233+ }
234+ return true ; // API request
235+ }
236+
237+ // Azure AD/authentication endpoints - never use PoP
238+ if ( host ? . Contains ( "login.microsoftonline.com" ) == true ||
239+ host ? . Contains ( "login.microsoft.com" ) == true ||
240+ host ? . Contains ( "login.chinacloudapi.cn" ) == true ||
241+ host ? . Contains ( "login.microsoftonline.de" ) == true ||
242+ host ? . Contains ( "login.microsoftonline.us" ) == true )
243+ {
244+ return false ; // Authentication request
245+ }
246+
247+ return false ; // Default to authentication request for unknown endpoints
248+ }
159249 }
160250}
0 commit comments