@@ -307,6 +307,167 @@ public async Task OAuth2_ExchangeToken_RetriesOnNetworkError() {
307307
308308 #endregion
309309
310+ #region OAuth2 Scopes
311+
312+ [ Fact ]
313+ public async Task OAuth2_ExchangeToken_IncludesScopesInRequest ( ) {
314+ var credentials = new Credentials {
315+ Method = CredentialsMethod . ClientCredentials ,
316+ Config = new CredentialsConfig {
317+ ClientId = TestClientId ,
318+ ClientSecret = TestClientSecret ,
319+ ApiTokenIssuer = TestTokenIssuer ,
320+ ApiAudience = TestAudience ,
321+ Scopes = "scope1 scope2"
322+ }
323+ } ;
324+ var retryParams = CreateTestRetryParams ( maxRetry : 0 , minWaitInMs : 10 ) ;
325+
326+ string ? requestBody = null ;
327+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
328+ mockHandler
329+ . Protected ( )
330+ . Setup < Task < HttpResponseMessage > > (
331+ "SendAsync" ,
332+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
333+ ItExpr . IsAny < CancellationToken > ( ) )
334+ . Returns ( async ( HttpRequestMessage request , CancellationToken ct ) => {
335+ requestBody = request . Content is null
336+ ? null
337+ : await request . Content . ReadAsStringAsync ( ) ;
338+ return CreateTokenResponse ( ) ;
339+ } ) ;
340+
341+ var httpClient = new HttpClient ( mockHandler . Object ) ;
342+ var configuration = new SdkConfiguration { ApiUrl = Constants . FgaConstants . TestApiUrl } ;
343+ var baseClient = new BaseClient ( configuration , httpClient ) ;
344+ var metrics = new Metrics ( configuration ) ;
345+
346+ var oauth2Client = new OAuth2Client ( credentials , baseClient , retryParams , metrics ) ;
347+ await oauth2Client . GetAccessTokenAsync ( ) ;
348+
349+ Assert . NotNull ( requestBody ) ;
350+ Assert . Contains ( "scope=scope1+scope2" , requestBody ) ;
351+ }
352+
353+ [ Fact ]
354+ public async Task OAuth2_ExchangeToken_OmitsScopeWhenNotConfigured ( ) {
355+ var credentials = CreateTestCredentials ( ) ;
356+ var retryParams = CreateTestRetryParams ( maxRetry : 0 , minWaitInMs : 10 ) ;
357+
358+ string ? requestBody = null ;
359+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
360+ mockHandler
361+ . Protected ( )
362+ . Setup < Task < HttpResponseMessage > > (
363+ "SendAsync" ,
364+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
365+ ItExpr . IsAny < CancellationToken > ( ) )
366+ . Returns ( async ( HttpRequestMessage request , CancellationToken ct ) => {
367+ requestBody = request . Content is null
368+ ? null
369+ : await request . Content . ReadAsStringAsync ( ) ;
370+ return CreateTokenResponse ( ) ;
371+ } ) ;
372+
373+ var httpClient = new HttpClient ( mockHandler . Object ) ;
374+ var configuration = new SdkConfiguration { ApiUrl = Constants . FgaConstants . TestApiUrl } ;
375+ var baseClient = new BaseClient ( configuration , httpClient ) ;
376+ var metrics = new Metrics ( configuration ) ;
377+
378+ var oauth2Client = new OAuth2Client ( credentials , baseClient , retryParams , metrics ) ;
379+ await oauth2Client . GetAccessTokenAsync ( ) ;
380+
381+ Assert . NotNull ( requestBody ) ;
382+ Assert . DoesNotContain ( "scope=" , requestBody ) ;
383+ }
384+
385+ [ Fact ]
386+ public async Task OAuth2_ExchangeToken_WorksWithoutAudience ( ) {
387+ var credentials = new Credentials {
388+ Method = CredentialsMethod . ClientCredentials ,
389+ Config = new CredentialsConfig {
390+ ClientId = TestClientId ,
391+ ClientSecret = TestClientSecret ,
392+ ApiTokenIssuer = TestTokenIssuer ,
393+ Scopes = "read write"
394+ }
395+ } ;
396+ var retryParams = CreateTestRetryParams ( maxRetry : 0 , minWaitInMs : 10 ) ;
397+
398+ string ? requestBody = null ;
399+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
400+ mockHandler
401+ . Protected ( )
402+ . Setup < Task < HttpResponseMessage > > (
403+ "SendAsync" ,
404+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
405+ ItExpr . IsAny < CancellationToken > ( ) )
406+ . Returns ( async ( HttpRequestMessage request , CancellationToken ct ) => {
407+ requestBody = request . Content is null
408+ ? null
409+ : await request . Content . ReadAsStringAsync ( ) ;
410+ return CreateTokenResponse ( ) ;
411+ } ) ;
412+
413+ var httpClient = new HttpClient ( mockHandler . Object ) ;
414+ var configuration = new SdkConfiguration { ApiUrl = Constants . FgaConstants . TestApiUrl } ;
415+ var baseClient = new BaseClient ( configuration , httpClient ) ;
416+ var metrics = new Metrics ( configuration ) ;
417+
418+ var oauth2Client = new OAuth2Client ( credentials , baseClient , retryParams , metrics ) ;
419+ var token = await oauth2Client . GetAccessTokenAsync ( ) ;
420+
421+ Assert . Equal ( "test-access-token" , token ) ;
422+ Assert . NotNull ( requestBody ) ;
423+ Assert . DoesNotContain ( "audience=" , requestBody ) ;
424+ Assert . Contains ( "scope=read+write" , requestBody ) ;
425+ }
426+
427+ [ Fact ]
428+ public async Task OAuth2_ExchangeToken_IncludesBothAudienceAndScopes ( ) {
429+ var credentials = new Credentials {
430+ Method = CredentialsMethod . ClientCredentials ,
431+ Config = new CredentialsConfig {
432+ ClientId = TestClientId ,
433+ ClientSecret = TestClientSecret ,
434+ ApiTokenIssuer = TestTokenIssuer ,
435+ ApiAudience = TestAudience ,
436+ Scopes = "openid profile"
437+ }
438+ } ;
439+ var retryParams = CreateTestRetryParams ( maxRetry : 0 , minWaitInMs : 10 ) ;
440+
441+ string ? requestBody = null ;
442+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
443+ mockHandler
444+ . Protected ( )
445+ . Setup < Task < HttpResponseMessage > > (
446+ "SendAsync" ,
447+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
448+ ItExpr . IsAny < CancellationToken > ( ) )
449+ . Returns ( async ( HttpRequestMessage request , CancellationToken ct ) => {
450+ requestBody = request . Content is null
451+ ? null
452+ : await request . Content . ReadAsStringAsync ( ) ;
453+ return CreateTokenResponse ( ) ;
454+ } ) ;
455+
456+ var httpClient = new HttpClient ( mockHandler . Object ) ;
457+ var configuration = new SdkConfiguration { ApiUrl = Constants . FgaConstants . TestApiUrl } ;
458+ var baseClient = new BaseClient ( configuration , httpClient ) ;
459+ var metrics = new Metrics ( configuration ) ;
460+
461+ var oauth2Client = new OAuth2Client ( credentials , baseClient , retryParams , metrics ) ;
462+ await oauth2Client . GetAccessTokenAsync ( ) ;
463+
464+ Assert . NotNull ( requestBody ) ;
465+ Assert . Contains ( "audience=test-audience" , requestBody ) ;
466+ Assert . Contains ( "scope=openid+profile" , requestBody ) ;
467+ }
468+
469+ #endregion
470+
310471 #region ApiTokenIssuer Path Handling
311472
312473 [ Theory ]
0 commit comments