55using System . Globalization ;
66using System . IO ;
77using System . Text ;
8+ using System . Text . Json ;
89using System . Threading ;
910using System . Threading . Tasks ;
11+ using Azure . Core ;
1012using Azure . Core . TestFramework ;
1113using Azure . Identity . Tests . Mock ;
1214using Microsoft . Identity . Client ;
@@ -36,8 +38,12 @@ public class CredentialTestBase : ClientTestBase
3638 protected string expectedCode ;
3739 protected DeviceCodeResult deviceCodeResult ;
3840
41+ protected const string DiscoveryResponseBody =
42+ "{\" tenant_discovery_endpoint\" : \" https://login.microsoftonline.com/c54fac88-3dd3-461f-a7c4-8a368e0340b3/v2.0/.well-known/openid-configuration\" ,\" api-version\" : \" 1.1\" ,\" metadata\" :[{\" preferred_network\" : \" login.microsoftonline.com\" ,\" preferred_cache\" : \" login.windows.net\" ,\" aliases\" :[\" login.microsoftonline.com\" ,\" login.windows.net\" ,\" login.microsoft.com\" ,\" sts.windows.net\" ]},{\" preferred_network\" : \" login.partner.microsoftonline.cn\" ,\" preferred_cache\" : \" login.partner.microsoftonline.cn\" ,\" aliases\" :[\" login.partner.microsoftonline.cn\" ,\" login.chinacloudapi.cn\" ]},{\" preferred_network\" : \" login.microsoftonline.de\" ,\" preferred_cache\" : \" login.microsoftonline.de\" ,\" aliases\" :[\" login.microsoftonline.de\" ]},{\" preferred_network\" : \" login.microsoftonline.us\" ,\" preferred_cache\" : \" login.microsoftonline.us\" ,\" aliases\" :[\" login.microsoftonline.us\" ,\" login.usgovcloudapi.net\" ]},{\" preferred_network\" : \" login-us.microsoftonline.com\" ,\" preferred_cache\" : \" login-us.microsoftonline.com\" ,\" aliases\" :[\" login-us.microsoftonline.com\" ]}]}" ;
43+
3944 public CredentialTestBase ( bool isAsync ) : base ( isAsync )
40- { }
45+ {
46+ }
4147
4248 public void TestSetup ( )
4349 {
@@ -57,7 +63,7 @@ public void TestSetup()
5763 TenantId ,
5864 new MockAccount ( "username" ) ,
5965 null ,
60- new [ ] { Scope } ,
66+ new [ ] { Scope } ,
6167 Guid . NewGuid ( ) ,
6268 null ,
6369 "Bearer" ) ;
@@ -103,7 +109,7 @@ public void TestSetup()
103109 TenantId ,
104110 new MockAccount ( "username" ) ,
105111 null ,
106- new [ ] { Scope } ,
112+ new [ ] { Scope } ,
107113 Guid . NewGuid ( ) ,
108114 null ,
109115 "Bearer" ) ;
@@ -150,6 +156,7 @@ protected async Task<string> ReadMockRequestContent(MockRequest request)
150156 {
151157 return null ;
152158 }
159+
153160 using var memoryStream = new MemoryStream ( ) ;
154161 request . Content . WriteTo ( memoryStream , CancellationToken . None ) ;
155162 memoryStream . Position = 0 ;
@@ -159,7 +166,8 @@ protected async Task<string> ReadMockRequestContent(MockRequest request)
159166 }
160167 }
161168
162- protected MockResponse CreateMockMsalTokenResponse ( int responseCode , string token , string tenantId , string userName )
169+ protected MockResponse CreateMockMsalTokenResponse ( int responseCode , string token , string tenantId ,
170+ string userName )
163171 {
164172 var response = new MockResponse ( responseCode ) ;
165173 var idToken = CreateMsalIdToken ( Guid . NewGuid ( ) . ToString ( ) , userName , tenantId ) ;
@@ -190,7 +198,7 @@ public static string CreateMsalIdToken(string uniqueId, string displayableId, st
190198 return string . Format ( CultureInfo . InvariantCulture , "someheader.{0}.somesignature" , MsalEncode ( id ) ) ;
191199 }
192200
193- private const char base64PadCharacter = '=' ;
201+ private const char base64PadCharacter = '=' ;
194202#if NET45
195203 private const string doubleBase64PadCharacter = "==" ;
196204#endif
@@ -204,11 +212,9 @@ public static string CreateMsalIdToken(string uniqueId, string displayableId, st
204212 /// </summary>
205213 internal static readonly char [ ] s_base64Table =
206214 {
207- 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' ,
208- 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' ,
209- '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' ,
210- base64UrlCharacter62 ,
211- base64UrlCharacter63
215+ 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' ,
216+ 'Z' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' ,
217+ 'y' , 'z' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , base64UrlCharacter62 , base64UrlCharacter63
212218 } ;
213219
214220 /// <summary>
@@ -302,7 +308,7 @@ private static string MsalEncode(byte[] inArray, int offset, int length)
302308 }
303309 break ;
304310
305- //default or case 0: no further operations are needed.
311+ //default or case 0: no further operations are needed.
306312 }
307313
308314 return new string ( output , 0 , j ) ;
@@ -323,5 +329,71 @@ public static string MsalEncode(byte[] inArray)
323329
324330 return MsalEncode ( inArray , 0 , inArray . Length ) ;
325331 }
332+
333+ protected bool RequestBodyHasUserAssertionWithHeader ( Request req , string headerName )
334+ {
335+ req . Content . TryComputeLength ( out var len ) ;
336+ byte [ ] content = new byte [ len ] ;
337+ var stream = new MemoryStream ( ( int ) len ) ;
338+ req . Content . WriteTo ( stream , default ) ;
339+ var body = Encoding . UTF8 . GetString ( stream . GetBuffer ( ) , 0 , ( int ) stream . Length ) ;
340+ var parts = body . Split ( '&' ) ;
341+ foreach ( var part in parts )
342+ {
343+ if ( part . StartsWith ( "client_assertion=" ) )
344+ {
345+ var assertion = part . AsSpan ( ) ;
346+ int start = assertion . IndexOf ( '=' ) + 1 ;
347+ assertion = assertion . Slice ( start ) ;
348+ int end = assertion . IndexOf ( '.' ) ;
349+ var jwt = assertion . Slice ( 0 , end ) ;
350+ string convertedToken = jwt . ToString ( ) . Replace ( '_' , '/' ) . Replace ( '-' , '+' ) ;
351+ switch ( jwt . Length % 4 )
352+ {
353+ case 2 :
354+ convertedToken += "==" ;
355+ break ;
356+ case 3 :
357+ convertedToken += "=" ;
358+ break ;
359+ }
360+
361+ Utf8JsonReader reader = new Utf8JsonReader ( Convert . FromBase64String ( convertedToken ) ) ;
362+ while ( reader . Read ( ) )
363+ {
364+ if ( reader . TokenType == JsonTokenType . PropertyName )
365+ {
366+ var header = reader . GetString ( ) ;
367+ if ( header == headerName )
368+ {
369+ return true ;
370+ }
371+
372+ reader . Read ( ) ;
373+ }
374+ }
375+ }
376+ }
377+
378+ return false ;
379+ }
380+
381+ protected MockTransport Createx5cValidatingTransport ( bool sendCertChain ) => new MockTransport ( ( req ) =>
382+ {
383+ // respond to tenant discovery
384+ if ( req . Uri . Path . StartsWith ( "/common/discovery" ) )
385+ {
386+ return new MockResponse ( 200 ) . SetContent ( DiscoveryResponseBody ) ;
387+ }
388+
389+ // respond to token request
390+ if ( req . Uri . Path . EndsWith ( "/token" ) )
391+ {
392+ Assert . That ( sendCertChain , Is . EqualTo ( RequestBodyHasUserAssertionWithHeader ( req , "x5c" ) ) ) ;
393+ return new MockResponse ( 200 ) . WithContent (
394+ $ "{{\" token_type\" : \" Bearer\" ,\" expires_in\" : 9999,\" ext_expires_in\" : 9999,\" access_token\" : \" { expectedToken } \" }}") ;
395+ }
396+ return new MockResponse ( 200 ) ;
397+ } ) ;
326398 }
327399}
0 commit comments