Skip to content

Commit 03f6627

Browse files
committed
CSHARP-3032: Support speculative SCRAM-SHA authentication attempts in isMaster
1 parent 88588c9 commit 03f6627

File tree

10 files changed

+525
-183
lines changed

10 files changed

+525
-183
lines changed

src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class DefaultAuthenticator : IAuthenticator
3535
// fields
3636
private readonly UsernamePasswordCredential _credential;
3737
private readonly IRandomStringGenerator _randomStringGenerator;
38+
private IAuthenticator _speculativeAuthenticator;
3839

3940
// constructors
4041
/// <summary>
@@ -79,9 +80,8 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
7980
description.BuildInfoResult);
8081
}
8182

82-
var authenticator = CreateAuthenticator(connection, description);
83+
var authenticator = GetOrCreateAuthenticator(connection, description);
8384
authenticator.Authenticate(connection, description, cancellationToken);
84-
8585
}
8686

8787
/// <inheritdoc/>
@@ -106,15 +106,18 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
106106
description.BuildInfoResult);
107107
}
108108

109-
var authenticator = CreateAuthenticator(connection, description);
109+
var authenticator = GetOrCreateAuthenticator(connection, description);
110110
await authenticator.AuthenticateAsync(connection, description, cancellationToken).ConfigureAwait(false);
111111
}
112112

113113

114114
/// <inheritdoc/>
115115
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
116116
{
117-
return isMasterCommand.Merge(CreateSaslSupportedMechsRequest(_credential.Source, _credential.Username));
117+
var saslSupportedMechs = CreateSaslSupportedMechsRequest(_credential.Source, _credential.Username);
118+
isMasterCommand = isMasterCommand.Merge(saslSupportedMechs);
119+
_speculativeAuthenticator = new ScramSha256Authenticator(_credential, _randomStringGenerator);
120+
return _speculativeAuthenticator.CustomizeInitialIsMasterCommand(isMasterCommand);
118121
}
119122

120123
private static BsonDocument CreateSaslSupportedMechsRequest(string authenticationDatabaseName, string userName)
@@ -143,5 +146,14 @@ private IAuthenticator CreateAuthenticator(IConnection connection, ConnectionDes
143146
: new MongoDBCRAuthenticator(_credential);
144147
#pragma warning restore 618
145148
}
149+
150+
private IAuthenticator GetOrCreateAuthenticator(IConnection connection, ConnectionDescription description)
151+
{
152+
/* It is possible to have for IsMaster["SpeculativeAuthenticate"] != null and for
153+
* _speculativeScramSha256Authenticator to be null in the case of multiple authenticators */
154+
var speculativeAuthenticateResult = description.IsMasterResult.SpeculativeAuthenticate;
155+
var canUseSpeculativeAuthenticator = _speculativeAuthenticator != null && speculativeAuthenticateResult != null;
156+
return canUseSpeculativeAuthenticator ? _speculativeAuthenticator : CreateAuthenticator(connection, description);
157+
}
146158
}
147-
}
159+
}

src/MongoDB.Driver.Core/Core/Authentication/SaslAuthenticator.cs

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ namespace MongoDB.Driver.Core.Authentication
3131
public abstract class SaslAuthenticator : IAuthenticator
3232
{
3333
// fields
34-
private readonly ISaslMechanism _mechanism;
34+
private protected readonly ISaslMechanism _mechanism;
35+
private protected ISaslStep _speculativeFirstStep;
3536

3637
// constructors
3738
/// <summary>
@@ -67,10 +68,20 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
6768

6869
using (var conversation = new SaslConversation(description.ConnectionId))
6970
{
70-
var currentStep = _mechanism.Initialize( connection, conversation, description);
71+
ISaslStep currentStep;
72+
BsonDocument command;
73+
var speculativeAuthenticateResult = description.IsMasterResult.SpeculativeAuthenticate;
74+
if (_speculativeFirstStep != null && speculativeAuthenticateResult != null)
75+
{
76+
currentStep = Transition(conversation, _speculativeFirstStep, speculativeAuthenticateResult, out command);
77+
}
78+
else
79+
{
80+
currentStep = _mechanism.Initialize(connection, conversation, description);
81+
command = CreateStartCommand(currentStep);
82+
}
7183

72-
var command = CreateStartCommand(currentStep);
73-
while (true)
84+
while (currentStep != null)
7485
{
7586
BsonDocument result;
7687
try
@@ -83,13 +94,7 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
8394
throw CreateException(connection, ex);
8495
}
8596

86-
currentStep = Transition(conversation, currentStep, result);
87-
if (currentStep == null)
88-
{
89-
return;
90-
}
91-
92-
command = CreateContinueCommand(currentStep, result);
97+
currentStep = Transition(conversation, currentStep, result , out command);
9398
}
9499
}
95100
}
@@ -102,10 +107,20 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
102107

103108
using (var conversation = new SaslConversation(description.ConnectionId))
104109
{
105-
var currentStep = _mechanism.Initialize(connection, conversation, description);
110+
ISaslStep currentStep;
111+
BsonDocument command;
112+
var speculativeAuthenticateResult = description.IsMasterResult.SpeculativeAuthenticate;
113+
if (_speculativeFirstStep != null && speculativeAuthenticateResult != null)
114+
{
115+
currentStep = Transition(conversation, _speculativeFirstStep, speculativeAuthenticateResult, out command);
116+
}
117+
else
118+
{
119+
currentStep = _mechanism.Initialize(connection, conversation, description);
120+
command = CreateStartCommand(currentStep);
121+
}
106122

107-
var command = CreateStartCommand(currentStep);
108-
while (true)
123+
while (currentStep != null)
109124
{
110125
BsonDocument result;
111126
try
@@ -118,23 +133,29 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
118133
throw CreateException(connection, ex);
119134
}
120135

121-
currentStep = Transition(conversation, currentStep, result);
122-
if (currentStep == null)
123-
{
124-
return;
125-
}
126-
127-
command = CreateContinueCommand(currentStep, result);
136+
currentStep = Transition(conversation, currentStep, result, out command);
128137
}
129138
}
130139
}
131140

132141
/// <inheritdoc/>
133-
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
142+
public virtual BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
134143
{
135144
return isMasterCommand;
136145
}
137146

147+
private protected virtual BsonDocument CreateStartCommand(ISaslStep currentStep)
148+
{
149+
var startCommand = new BsonDocument
150+
{
151+
{ "saslStart", 1 },
152+
{ "mechanism", _mechanism.Name },
153+
{ "payload", currentStep.BytesToSendToServer }
154+
};
155+
156+
return startCommand;
157+
}
158+
138159
private CommandWireProtocol<BsonDocument> CreateCommandProtocol(BsonDocument command)
139160
{
140161
return new CommandWireProtocol<BsonDocument>(
@@ -161,28 +182,16 @@ private MongoAuthenticationException CreateException(IConnection connection, Exc
161182
return new MongoAuthenticationException(connection.ConnectionId, message, ex);
162183
}
163184

164-
private BsonDocument CreateStartCommand(ISaslStep currentStep)
165-
{
166-
var startCommand = new BsonDocument
167-
{
168-
{ "saslStart", 1 },
169-
{ "mechanism", _mechanism.Name },
170-
{ "payload", currentStep.BytesToSendToServer }
171-
};
172-
173-
if (_mechanism.Name.StartsWith("SCRAM", StringComparison.OrdinalIgnoreCase))
174-
{
175-
startCommand.Add("options", new BsonDocument("skipEmptyExchange", true));
176-
}
177-
178-
return startCommand;
179-
}
180-
181-
private ISaslStep Transition(SaslConversation conversation, ISaslStep currentStep, BsonDocument result)
185+
private ISaslStep Transition(
186+
SaslConversation conversation,
187+
ISaslStep currentStep,
188+
BsonDocument result,
189+
out BsonDocument command)
182190
{
183191
// we might be done here if the client is not expecting a reply from the server
184192
if (result.GetValue("done", false).ToBoolean() && currentStep.IsComplete)
185193
{
194+
command = null;
186195
return null;
187196
}
188197

@@ -191,12 +200,15 @@ private ISaslStep Transition(SaslConversation conversation, ISaslStep currentSte
191200
// we might be done here if the client had some final verification it needed to do
192201
if (result.GetValue("done", false).ToBoolean() && currentStep.IsComplete)
193202
{
203+
command = null;
194204
return null;
195205
}
196206

207+
command = CreateContinueCommand(currentStep, result);
197208
return currentStep;
198209
}
199210

211+
200212
// nested classes
201213
/// <summary>
202214
/// Represents a SASL conversation.

src/MongoDB.Driver.Core/Core/Authentication/ScramShaAuthenticator.cs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.Security.Cryptography;
1818
using System.Text;
19+
using MongoDB.Bson;
1920
using MongoDB.Bson.IO;
2021
#if NET452
2122
using MongoDB.Driver.Core.Authentication.Vendored;
@@ -35,7 +36,7 @@ public abstract class ScramShaAuthenticator : SaslAuthenticator
3536
/// </summary>
3637
/// <param name="data">The data to hash. Also called "str" in RFC5802.</param>
3738
protected internal delegate byte[] H(byte[] data);
38-
39+
3940
/// <summary>
4041
/// A Hi function used to compute the SaltedPassword as defined in RFC5802, except with "str" parameter replaced
4142
/// with a UsernamePassword credential so that the password can be optionally digested/prepped in a secure fashion
@@ -46,15 +47,15 @@ public abstract class ScramShaAuthenticator : SaslAuthenticator
4647
/// <param name="salt">The salt.</param>
4748
/// <param name="iterations">The iteration count.</param>
4849
protected internal delegate byte[] Hi(UsernamePasswordCredential credentials, byte[] salt, int iterations);
49-
50+
5051
/// <summary>
5152
/// An HMAC function as defined in RFC5802, plus the encoding of the data.
5253
/// </summary>
5354
/// <param name="encoding">The encoding of the data.</param>
5455
/// <param name="data">The data. Also called "str" in RFC5802.</param>
5556
/// <param name="key">The key.</param>
5657
protected internal delegate byte[] Hmac(UTF8Encoding encoding, byte[] data, string key);
57-
58+
5859
// fields
5960
private readonly string _databaseName;
6061

@@ -67,7 +68,7 @@ public abstract class ScramShaAuthenticator : SaslAuthenticator
6768
/// <param name="h">The H function to use.</param>
6869
/// <param name="hi">The Hi function to use.</param>
6970
/// <param name="hmac">The Hmac function to use.</param>
70-
protected ScramShaAuthenticator(UsernamePasswordCredential credential,
71+
protected ScramShaAuthenticator(UsernamePasswordCredential credential,
7172
HashAlgorithmName hashAlgorithmName,
7273
H h,
7374
Hi hi,
@@ -85,7 +86,7 @@ protected ScramShaAuthenticator(UsernamePasswordCredential credential,
8586
/// <param name="hmac">The Hmac function to use.</param>
8687
/// <param name="cache">The cache to use.</param>
8788
internal ScramShaAuthenticator(
88-
UsernamePasswordCredential credential,
89+
UsernamePasswordCredential credential,
8990
HashAlgorithmName hashAlgorithName,
9091
IRandomStringGenerator randomStringGenerator,
9192
H h,
@@ -101,6 +102,24 @@ internal ScramShaAuthenticator(
101102
/// <inheritdoc/>
102103
public override string DatabaseName => _databaseName;
103104

105+
/// <inheritdoc/>
106+
public override BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
107+
{
108+
isMasterCommand = base.CustomizeInitialIsMasterCommand(isMasterCommand);
109+
_speculativeFirstStep = _mechanism.Initialize(connection: null, conversation: null, description: null);
110+
var firstCommand = CreateStartCommand(_speculativeFirstStep);
111+
firstCommand.Add("db", DatabaseName);
112+
isMasterCommand.Add("speculativeAuthenticate", firstCommand);
113+
return isMasterCommand;
114+
}
115+
116+
private protected override BsonDocument CreateStartCommand(ISaslStep currentStep)
117+
{
118+
var startCommand = base.CreateStartCommand(currentStep);
119+
startCommand.Add("options", new BsonDocument("skipEmptyExchange", true));
120+
return startCommand;
121+
}
122+
104123
// nested classes
105124
private class ScramShaMechanism : ISaslMechanism
106125
{
@@ -113,8 +132,8 @@ private class ScramShaMechanism : ISaslMechanism
113132
private ScramCache _cache;
114133

115134
public ScramShaMechanism(
116-
UsernamePasswordCredential credential,
117-
HashAlgorithmName hashAlgorithmName,
135+
UsernamePasswordCredential credential,
136+
HashAlgorithmName hashAlgorithmName,
118137
IRandomStringGenerator randomStringGenerator,
119138
H h,
120139
Hi hi,
@@ -138,9 +157,6 @@ public ScramShaMechanism(
138157

139158
public ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description)
140159
{
141-
Ensure.IsNotNull(connection, nameof(connection));
142-
Ensure.IsNotNull(description, nameof(description));
143-
144160
const string gs2Header = "n,,";
145161
var username = "n=" + PrepUsername(_credential.Username);
146162
var r = GenerateRandomString();
@@ -165,13 +181,13 @@ private string PrepUsername(string username)
165181
return username.Replace("=", "=3D").Replace(",", "=2C");
166182
}
167183
}
168-
184+
169185
private class ClientFirst : ISaslStep
170186
{
171187
private readonly byte[] _bytesToSendToServer;
172188
private readonly string _clientFirstMessageBare;
173189
private readonly UsernamePasswordCredential _credential;
174-
190+
175191
private readonly string _rPrefix;
176192
private readonly H _h;
177193
private readonly Hi _hi;
@@ -180,9 +196,9 @@ private class ClientFirst : ISaslStep
180196
private ScramCache _cache;
181197

182198
public ClientFirst(
183-
byte[] bytesToSendToServer,
184-
string clientFirstMessageBare,
185-
UsernamePasswordCredential credential,
199+
byte[] bytesToSendToServer,
200+
string clientFirstMessageBare,
201+
UsernamePasswordCredential credential,
186202
string rPrefix,
187203
H h,
188204
Hi hi,
@@ -266,7 +282,7 @@ private byte[] XOR(byte[] a, byte[] b)
266282
}
267283

268284
private class ClientLast : ISaslStep
269-
{
285+
{
270286
private readonly byte[] _bytesToSendToServer;
271287
private readonly byte[] _serverSignature64;
272288

@@ -293,7 +309,7 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF
293309

294310
return new CompletedStep();
295311
}
296-
312+
297313
private bool ConstantTimeEquals(byte[] a, byte[] b)
298314
{
299315
var diff = a.Length ^ b.Length;
@@ -306,4 +322,4 @@ private bool ConstantTimeEquals(byte[] a, byte[] b)
306322
}
307323
}
308324
}
309-
}
325+
}

0 commit comments

Comments
 (0)