5
5
using System . Globalization ;
6
6
using System . IO ;
7
7
using System . Text ;
8
+ using System . Text . Json ;
8
9
using System . Threading ;
9
10
using System . Threading . Tasks ;
11
+ using Azure . Core ;
10
12
using Azure . Core . TestFramework ;
11
13
using Azure . Identity . Tests . Mock ;
12
14
using Microsoft . Identity . Client ;
@@ -36,8 +38,12 @@ public class CredentialTestBase : ClientTestBase
36
38
protected string expectedCode ;
37
39
protected DeviceCodeResult deviceCodeResult ;
38
40
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
+
39
44
public CredentialTestBase ( bool isAsync ) : base ( isAsync )
40
- { }
45
+ {
46
+ }
41
47
42
48
public void TestSetup ( )
43
49
{
@@ -57,7 +63,7 @@ public void TestSetup()
57
63
TenantId ,
58
64
new MockAccount ( "username" ) ,
59
65
null ,
60
- new [ ] { Scope } ,
66
+ new [ ] { Scope } ,
61
67
Guid . NewGuid ( ) ,
62
68
null ,
63
69
"Bearer" ) ;
@@ -103,7 +109,7 @@ public void TestSetup()
103
109
TenantId ,
104
110
new MockAccount ( "username" ) ,
105
111
null ,
106
- new [ ] { Scope } ,
112
+ new [ ] { Scope } ,
107
113
Guid . NewGuid ( ) ,
108
114
null ,
109
115
"Bearer" ) ;
@@ -150,6 +156,7 @@ protected async Task<string> ReadMockRequestContent(MockRequest request)
150
156
{
151
157
return null ;
152
158
}
159
+
153
160
using var memoryStream = new MemoryStream ( ) ;
154
161
request . Content . WriteTo ( memoryStream , CancellationToken . None ) ;
155
162
memoryStream . Position = 0 ;
@@ -159,7 +166,8 @@ protected async Task<string> ReadMockRequestContent(MockRequest request)
159
166
}
160
167
}
161
168
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 )
163
171
{
164
172
var response = new MockResponse ( responseCode ) ;
165
173
var idToken = CreateMsalIdToken ( Guid . NewGuid ( ) . ToString ( ) , userName , tenantId ) ;
@@ -190,7 +198,7 @@ public static string CreateMsalIdToken(string uniqueId, string displayableId, st
190
198
return string . Format ( CultureInfo . InvariantCulture , "someheader.{0}.somesignature" , MsalEncode ( id ) ) ;
191
199
}
192
200
193
- private const char base64PadCharacter = '=' ;
201
+ private const char base64PadCharacter = '=' ;
194
202
#if NET45
195
203
private const string doubleBase64PadCharacter = "==" ;
196
204
#endif
@@ -204,11 +212,9 @@ public static string CreateMsalIdToken(string uniqueId, string displayableId, st
204
212
/// </summary>
205
213
internal static readonly char [ ] s_base64Table =
206
214
{
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
212
218
} ;
213
219
214
220
/// <summary>
@@ -302,7 +308,7 @@ private static string MsalEncode(byte[] inArray, int offset, int length)
302
308
}
303
309
break ;
304
310
305
- //default or case 0: no further operations are needed.
311
+ //default or case 0: no further operations are needed.
306
312
}
307
313
308
314
return new string ( output , 0 , j ) ;
@@ -323,5 +329,71 @@ public static string MsalEncode(byte[] inArray)
323
329
324
330
return MsalEncode ( inArray , 0 , inArray . Length ) ;
325
331
}
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
+ } ) ;
326
398
}
327
399
}
0 commit comments