27
27
using Xunit ;
28
28
using MongoDB . Driver . Core . Connections ;
29
29
using MongoDB . Bson . TestHelpers . XunitExtensions ;
30
+
30
31
namespace MongoDB . Driver . Core . Authentication
31
32
{
32
33
public class ScramSha256AuthenticatorTests
33
34
{
34
- private static readonly UsernamePasswordCredential __credential
35
- = new UsernamePasswordCredential ( "source" , "user" , "pencil" ) ;
35
+ // private constants
36
+ private const string _clientNonce = "rOprNGfwEbeRWgbNEkqO" ;
37
+ private const int _iterationCount = 4096 ;
38
+ private const string _serverNonce = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" ;
39
+ private const string _serverSalt = "W22ZaJ0SNY7soEsUEjb6gQ==" ;
40
+
41
+ // private static
42
+ private static readonly UsernamePasswordCredential __credential = new UsernamePasswordCredential ( "source" , "user" , "pencil" ) ;
36
43
private static readonly ClusterId __clusterId = new ClusterId ( ) ;
37
44
private static readonly ServerId __serverId = new ServerId ( __clusterId , new DnsEndPoint ( "localhost" , 27017 ) ) ;
38
45
private static readonly ConnectionDescription __description = new ConnectionDescription (
39
46
new ConnectionId ( __serverId ) ,
40
47
new IsMasterResult ( new BsonDocument ( "ok" , 1 ) . Add ( "ismaster" , 1 ) ) ,
41
48
new BuildInfoResult ( new BsonDocument ( "version" , "4.0.0" ) ) ) ;
42
-
49
+
43
50
/*
44
51
* This is a simple example of a SCRAM-SHA-256 authentication exchange. The username
45
52
* 'user' and password 'pencil' are being used, with a client nonce of "rOprNGfwEbeRWgbNEkqO"
@@ -49,42 +56,42 @@ private static readonly UsernamePasswordCredential __credential
49
56
* S: v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
50
57
*/
51
58
52
- private const string _clientNonce = "rOprNGfwEbeRWgbNEkqO" ;
53
- private const int _iterationCount = 4096 ;
54
- private const string _serverNonce = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" ;
55
- private const string _serverSalt = "W22ZaJ0SNY7soEsUEjb6gQ==" ;
56
-
57
59
private static readonly string _clientRequest1 = $ "n,,n=user,r={ _clientNonce } ";
58
60
private static readonly string _serverResponse1 =
59
61
$ "r={ _clientNonce } { _serverNonce } ,s={ _serverSalt } ,i={ _iterationCount } ";
60
62
private static readonly string _clientRequest2 =
61
63
$ "c=biws,r={ _clientNonce } { _serverNonce } ,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=";
62
64
private static readonly string _serverReponse2 = "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=" ;
63
-
65
+
64
66
private static void Authenticate (
65
67
ScramSha256Authenticator authenticator ,
66
68
IConnection connection ,
67
- ConnectionDescription description ,
68
69
bool async )
69
70
{
70
71
if ( async)
71
72
{
72
- authenticator . AuthenticateAsync ( connection , __description , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
73
+ authenticator
74
+ . AuthenticateAsync ( connection , __description , CancellationToken . None )
75
+ . GetAwaiter ( )
76
+ . GetResult ( ) ;
77
+ }
78
+ else
79
+ {
80
+ authenticator . Authenticate ( connection , __description , CancellationToken . None ) ;
73
81
}
74
- authenticator . Authenticate ( connection , __description , CancellationToken . None ) ;
75
82
}
76
-
83
+
77
84
/* In response, the server sends a "server-first-message" containing the
78
85
* user's iteration count i and the user's salt, and appends its own
79
86
* nonce to the client-specified one. */
80
87
private static string CreateSaslStartReply (
81
- string clientSaslStart ,
82
- string serverNonce ,
83
- string serverSalt ,
88
+ string clientSaslStart ,
89
+ string serverNonce ,
90
+ string serverSalt ,
84
91
int iterationCount )
85
92
{
86
93
// strip expected GS2 header of "n,," before parsing map
87
- var clientNonce = SaslMapParser . Parse ( clientSaslStart . Substring ( 3 ) ) [ 'r' ] ;
94
+ var clientNonce = SaslMapParser . Parse ( clientSaslStart . Substring ( 3 ) ) [ 'r' ] ;
88
95
return $ "r={ clientNonce } { serverNonce } ,s={ serverSalt } ,i={ iterationCount } ";
89
96
}
90
97
@@ -93,15 +100,15 @@ private static string PoisonSaslMessage(string message, string poison)
93
100
{
94
101
return message . Substring ( 0 , message . Length - poison . Length ) + poison ;
95
102
}
96
-
103
+
97
104
private static string ToUtf8Base64 ( string s )
98
105
{
99
106
return Convert . ToBase64String ( ( System . Text . Encoding . UTF8 . GetBytes ( s ) ) ) ;
100
107
}
101
-
108
+
102
109
[ Fact ]
103
110
public void Constructor_should_throw_an_ArgumentNullException_when_credential_is_null ( )
104
- {
111
+ {
105
112
var exception = Record . Exception ( ( ) => new ScramSha256Authenticator ( null ) ) ;
106
113
exception . Should ( ) . BeOfType < ArgumentNullException > ( ) ;
107
114
}
@@ -117,12 +124,12 @@ public void Authenticate_should_throw_an_AuthenticationException_when_authentica
117
124
var connection = new MockConnection ( __serverId ) ;
118
125
connection . EnqueueReplyMessage ( reply ) ;
119
126
120
- var act = async
127
+ var act = async
121
128
? ( ) => subject . AuthenticateAsync ( connection , __description , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( )
122
- : ( Action ) ( ( ) => subject . Authenticate ( connection , __description , CancellationToken . None ) ) ;
129
+ : ( Action ) ( ( ) => subject . Authenticate ( connection , __description , CancellationToken . None ) ) ;
123
130
124
131
var exception = Record . Exception ( act ) ;
125
-
132
+
126
133
exception . Should ( ) . BeOfType < MongoAuthenticationException > ( ) ;
127
134
}
128
135
@@ -136,20 +143,20 @@ public void Authenticate_should_throw_when_server_provides_invalid_r_value(
136
143
var poisonedSaslStart = PoisonSaslMessage ( message : _clientRequest1 , poison : "bluePill" ) ;
137
144
var poisonedSaslStartReply = CreateSaslStartReply ( poisonedSaslStart , _serverNonce , _serverSalt , _iterationCount ) ;
138
145
var poisonedSaslStartReplyMessage = MessageHelper . BuildReply ( RawBsonDocumentHelper . FromJson (
139
- @"{conversationId: 1, " +
146
+ @"{conversationId: 1, " +
140
147
$ " payload: BinData(0,\" { ToUtf8Base64 ( poisonedSaslStartReply ) } \" )," +
141
148
@" done: false,
142
149
ok: 1}" ) ) ;
143
150
144
151
var connection = new MockConnection ( __serverId ) ;
145
152
connection . EnqueueReplyMessage ( poisonedSaslStartReplyMessage ) ;
146
153
147
- var act = async
154
+ var act = async
148
155
? ( ) => subject . AuthenticateAsync ( connection , __description , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( )
149
- : ( Action ) ( ( ) => subject . Authenticate ( connection , __description , CancellationToken . None ) ) ;
156
+ : ( Action ) ( ( ) => subject . Authenticate ( connection , __description , CancellationToken . None ) ) ;
150
157
151
158
var exception = Record . Exception ( act ) ;
152
-
159
+
153
160
exception . Should ( ) . BeOfType < MongoAuthenticationException > ( ) ;
154
161
}
155
162
@@ -160,17 +167,17 @@ public void Authenticate_should_throw_when_server_provides_invalid_serverSignatu
160
167
{
161
168
var randomStringGenerator = new ConstantRandomStringGenerator ( _clientNonce ) ;
162
169
var subject = new ScramSha256Authenticator ( __credential , randomStringGenerator ) ;
163
-
170
+
164
171
var saslStartReply = CreateSaslStartReply ( _clientRequest1 , _serverNonce , _serverSalt , _iterationCount ) ;
165
- var poisonedSaslContinueReply = PoisonSaslMessage ( message : _serverReponse2 , poison : "redApple" ) ;
172
+ var poisonedSaslContinueReply = PoisonSaslMessage ( message : _serverReponse2 , poison : "redApple" ) ;
166
173
var saslStartReplyMessage = MessageHelper . BuildReply ( RawBsonDocumentHelper . FromJson (
167
- @"{conversationId: 1, " +
174
+ @"{conversationId: 1, " +
168
175
$ " payload: BinData(0,\" { ToUtf8Base64 ( saslStartReply ) } \" )," +
169
176
@" done: false,
170
177
ok: 1}" ) ) ;
171
178
var poisonedSaslContinueReplyMessage = MessageHelper . BuildReply ( RawBsonDocumentHelper . FromJson (
172
- @"{conversationId: 1, " +
173
- $ " payload: BinData(0,\" { ToUtf8Base64 ( poisonedSaslContinueReply ) } \" )," +
179
+ @"{conversationId: 1, " +
180
+ $ " payload: BinData(0,\" { ToUtf8Base64 ( poisonedSaslContinueReply ) } \" )," +
174
181
@" done: true,
175
182
ok: 1}" ) ) ;
176
183
@@ -187,7 +194,7 @@ public void Authenticate_should_throw_when_server_provides_invalid_serverSignatu
187
194
{
188
195
act = ( ) => subject . Authenticate ( connection , __description , CancellationToken . None ) ;
189
196
}
190
-
197
+
191
198
var exception = Record . Exception ( act ) ;
192
199
193
200
exception. Should ( ) . BeOfType < MongoAuthenticationException > ( ) ;
@@ -202,13 +209,13 @@ public void Authenticate_should_not_throw_when_authentication_succeeds(
202
209
var subject = new ScramSha256Authenticator ( __credential , randomStringGenerator ) ;
203
210
204
211
var saslStartReply = MessageHelper. BuildReply < RawBsonDocument > ( RawBsonDocumentHelper . FromJson (
205
- @"{conversationId: 1," +
212
+ @"{conversationId: 1," +
206
213
$ " payload: BinData(0,\" { ToUtf8Base64 ( _serverResponse1 ) } \" )," +
207
214
@" done: false,
208
215
ok: 1}" ) ) ;
209
216
var saslContinueReply = MessageHelper. BuildReply < RawBsonDocument > ( RawBsonDocumentHelper . FromJson (
210
217
@"{conversationId: 1," +
211
- $ " payload: BinData(0,\" { ToUtf8Base64 ( _serverReponse2 ) } \" )," +
218
+ $ " payload: BinData(0,\" { ToUtf8Base64 ( _serverReponse2 ) } \" )," +
212
219
@" done: true,
213
220
ok: 1}" ) ) ;
214
221
@@ -241,24 +248,24 @@ public void Authenticate_should_not_throw_when_authentication_succeeds(
241
248
actualRequestId1. Should ( ) . BeInRange ( actualRequestId0 + 1 , actualRequestId0 + 11 ) ;
242
249
243
250
sentMessages[ 0 ] . Should ( ) . Be (
244
- @"{opcode: ""query""," +
251
+ @"{opcode: ""query""," +
245
252
$ " requestId: { actualRequestId0 } ," +
246
253
@" database: ""source"",
247
254
collection: ""$cmd"",
248
255
batchSize: -1,
249
256
slaveOk: true,
250
257
query: {saslStart: 1,
251
- mechanism: ""SCRAM-SHA-256""," +
258
+ mechanism: ""SCRAM-SHA-256""," +
252
259
$ " payload: new BinData(0, \" { ToUtf8Base64 ( _clientRequest1 ) } \" )}}}}") ;
253
260
sentMessages[ 1 ] . Should ( ) . Be (
254
- @"{opcode: ""query""," +
261
+ @"{opcode: ""query""," +
255
262
$ " requestId: { actualRequestId1 } ," +
256
263
@" database: ""source"",
257
264
collection: ""$cmd"",
258
265
batchSize: -1,
259
266
slaveOk: true,
260
267
query: {saslContinue: 1,
261
- conversationId: 1, " +
268
+ conversationId: 1, " +
262
269
$ " payload: new BinData(0, \" { ToUtf8Base64 ( _clientRequest2 ) } \" )}}}}") ;
263
270
}
264
271
@@ -302,6 +309,54 @@ public void Authenticate_should_use_cache(
302
309
subject . _cache ( ) . _cacheKey ( ) . Should ( ) . NotBe ( null ) ;
303
310
subject . _cache ( ) . _cachedEntry ( ) . Should ( ) . NotBe ( null ) ;
304
311
}
312
+
313
+ #if ! NETCOREAPP1_1
314
+ [ Theory ]
315
+ [ ParameterAttributeData ]
316
+ public void Authenticate_should_work_regardless_of_culture(
317
+ [ Values ( "da-DK" , "en-US" ) ] string name ,
318
+ [ Values ( false , true ) ] bool async )
319
+ {
320
+ SetCultureAndResetAfterTest ( name , ( ) =>
321
+ {
322
+ var randomStringGenerator = new ConstantRandomStringGenerator ( "a" ) ;
323
+
324
+ // ScramSha1Authenticator will have exactly the same code paths
325
+ var subject = new ScramSha256Authenticator ( __credential , randomStringGenerator ) ;
326
+ var mockConnection = new MockConnection ( ) ;
327
+
328
+ var payload1 = $ "r=aa,s={ _serverSalt } ,i=1";
329
+ var serverResponse1 = $ "{{ ok : 1, payload : BinData(0,\" { ToUtf8Base64 ( payload1 ) } \" ), done : true, conversationId : 1 }}";
330
+ var serverResponseRawDocument1 = RawBsonDocumentHelper . FromJson ( serverResponse1 ) ;
331
+ var serverResponseMessage1 = MessageHelper . BuildReply ( serverResponseRawDocument1 ) ;
332
+
333
+ var payload2 = $ "v=v1wZS02d7kZVSzuKoB7TuI+jIpSsKvnQUkU9Oqj2t+w=";
334
+ var serverResponse2 = $ "{{ ok : 1, payload : BinData(0,\" { ToUtf8Base64 ( payload2 ) } \" ), done : true }}";
335
+ var serverResponseRawDocument2 = RawBsonDocumentHelper . FromJson ( serverResponse2 ) ;
336
+ var serverResponseMessage2 = MessageHelper . BuildReply ( serverResponseRawDocument2 ) ;
337
+
338
+ mockConnection . EnqueueReplyMessage ( serverResponseMessage1 ) ;
339
+ mockConnection . EnqueueReplyMessage ( serverResponseMessage2 ) ;
340
+
341
+ Authenticate ( subject , mockConnection , async ) ;
342
+ } ) ;
343
+
344
+ void SetCultureAndResetAfterTest ( string cultureName , Action test )
345
+ {
346
+ var originalCulture = Thread . CurrentThread . CurrentCulture ;
347
+ Thread . CurrentThread . CurrentCulture = new System . Globalization . CultureInfo ( cultureName ) ;
348
+
349
+ try
350
+ {
351
+ test ( ) ;
352
+ }
353
+ finally
354
+ {
355
+ Thread . CurrentThread . CurrentCulture = originalCulture ;
356
+ }
357
+ }
358
+ }
359
+ #endif
305
360
}
306
361
307
362
internal static class ScramShaAuthenticatorReflector
@@ -313,10 +368,10 @@ public static ScramCache _cache(this ScramShaAuthenticator obj) =>
313
368
314
369
internal static class ScramCacheReflector
315
370
{
316
- public static ScramCacheKey _cacheKey ( this ScramCache obj ) =>
371
+ public static ScramCacheKey _cacheKey ( this ScramCache obj ) =>
317
372
( ScramCacheKey ) Reflector . GetFieldValue ( obj , nameof ( _cacheKey ) ) ;
318
373
319
- public static ScramCacheEntry _cachedEntry ( this ScramCache obj) =>
374
+ public static ScramCacheEntry _cachedEntry ( this ScramCache obj ) =>
320
375
( ScramCacheEntry ) Reflector . GetFieldValue ( obj , nameof ( _cachedEntry ) ) ;
321
376
}
322
377
}
0 commit comments