Skip to content

Commit 4892dcc

Browse files
committed
CSHARP-2171: SCRAM-SHA-256 Support
1 parent dff7de8 commit 4892dcc

33 files changed

+3942
-363
lines changed

THIRD-PARTY-NOTICES

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
The MongoDB .NET Driver uses third-party libraries or other resources that may
2+
be distributed under licenses different than the MongoDB .NET Driver software.
3+
4+
In the event that we accidentally failed to list a required notice,
5+
please bring it to our attention through any of the ways detailed here:
6+
7+
8+
9+
The attached notices are provided for information only.
10+
11+
For any licenses that require disclosure of source, sources are available at
12+
https://github.com/mongodb/mongo-csharp-driver.
13+
14+
15+
1) The following files: CryptographyHelpers.cs, HashAlgorithmName.cs, Rfc2898DeriveBytes.cs
16+
17+
Original work:
18+
Copyright (c) 2016–2017 .NET Foundation and Contributors
19+
The MIT License (MIT)
20+
21+
Copyright (c) .NET Foundation and Contributors
22+
23+
All rights reserved.
24+
25+
Permission is hereby granted, free of charge, to any person obtaining a copy
26+
of this software and associated documentation files (the "Software"), to deal
27+
in the Software without restriction, including without limitation the rights
28+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29+
copies of the Software, and to permit persons to whom the Software is
30+
furnished to do so, subject to the following conditions:
31+
32+
The above copyright notice and this permission notice shall be included in all
33+
copies or substantial portions of the Software.
34+
35+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41+
SOFTWARE.
42+
43+
Modified work:
44+
Copyright (c) 2018–present MongoDB Inc.
45+
46+
Licensed under the Apache License, Version 2.0 (the "License");
47+
you may not use this file except in compliance with the License.
48+
You may obtain a copy of the License at
49+
50+
http://www.apache.org/licenses/LICENSE-2.0
51+
52+
Unless required by applicable law or agreed to in writing, software
53+
distributed under the License is distributed on an "AS IS" BASIS,
54+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
55+
See the License for the specific language governing permissions and
56+
limitations under the License.

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

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@
1414
*/
1515

1616
using System;
17+
using System.Linq;
1718
using System.Threading;
1819
using System.Threading.Tasks;
20+
using MongoDB.Bson;
1921
using MongoDB.Driver.Core.Connections;
2022
using MongoDB.Driver.Core.Misc;
2123

2224
namespace MongoDB.Driver.Core.Authentication
2325
{
2426
/// <summary>
25-
/// The default authenticator (uses SCRAM-SHA1 if possible, falls back to MONGODB-CR otherwise).
27+
/// The default authenticator.
28+
/// If saslSupportedMechs is not present in the isMaster results for mechanism negotiation
29+
/// uses SCRAM-SHA-1 when talking to servers >= 3.0. Prior to server 3.0, uses MONGODB-CR.
30+
/// Else, uses SCRAM-SHA-256 if present in the list of mechanisms. Otherwise, uses
31+
/// SCRAM-SHA-1 the default, regardless of whether SCRAM-SHA-1 is in the list.
2632
/// </summary>
2733
public class DefaultAuthenticator : IAuthenticator
2834
{
@@ -48,10 +54,7 @@ internal DefaultAuthenticator(UsernamePasswordCredential credential, IRandomStri
4854

4955
// properties
5056
/// <inheritdoc/>
51-
public string Name
52-
{
53-
get { return "DEFAULT"; }
54-
}
57+
public string Name => "DEFAULT";
5558

5659
// methods
5760
/// <inheritdoc/>
@@ -60,30 +63,83 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
6063
Ensure.IsNotNull(connection, nameof(connection));
6164
Ensure.IsNotNull(description, nameof(description));
6265

63-
var authenticator = CreateAuthenticator(description);
66+
// If we don't have SaslSupportedMechs as part of the response, that means we didn't piggyback the initial
67+
// isMaster request and should query the server (provided that the server >= 4.0), merging results into
68+
// a new ConnectionDescription
69+
if (!description.IsMasterResult.HasSaslSupportedMechs
70+
&& Feature.ScramSha256Authentication.IsSupported(description.ServerVersion))
71+
{
72+
var command = CustomizeInitialIsMasterCommand(IsMasterHelper.CreateCommand());
73+
var isMasterProtocol = IsMasterHelper.CreateProtocol(command);
74+
var isMasterResult = IsMasterHelper.GetResult(connection, isMasterProtocol, cancellationToken);
75+
var mergedIsMasterResult = new IsMasterResult(description.IsMasterResult.Wrapped.Merge(isMasterResult.Wrapped));
76+
description = new ConnectionDescription(
77+
description.ConnectionId,
78+
mergedIsMasterResult,
79+
description.BuildInfoResult);
80+
}
81+
82+
var authenticator = CreateAuthenticator(connection, description);
6483
authenticator.Authenticate(connection, description, cancellationToken);
84+
6585
}
6686

6787
/// <inheritdoc/>
68-
public Task AuthenticateAsync(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken)
88+
public async Task AuthenticateAsync(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken)
6989
{
7090
Ensure.IsNotNull(connection, nameof(connection));
7191
Ensure.IsNotNull(description, nameof(description));
92+
93+
// If we don't have SaslSupportedMechs as part of the response, that means we didn't piggyback the initial
94+
// isMaster request and should query the server (provided that the server >= 4.0), merging results into
95+
// a new ConnectionDescription
96+
if (!description.IsMasterResult.HasSaslSupportedMechs
97+
&& Feature.ScramSha256Authentication.IsSupported(description.ServerVersion))
98+
{
99+
var command = CustomizeInitialIsMasterCommand(IsMasterHelper.CreateCommand());
100+
var isMasterProtocol = IsMasterHelper.CreateProtocol(command);
101+
var isMasterResult = await IsMasterHelper.GetResultAsync(connection, isMasterProtocol, cancellationToken).ConfigureAwait(false);
102+
var mergedIsMasterResult = new IsMasterResult(description.IsMasterResult.Wrapped.Merge(isMasterResult.Wrapped));
103+
description = new ConnectionDescription(
104+
description.ConnectionId,
105+
mergedIsMasterResult,
106+
description.BuildInfoResult);
107+
}
108+
109+
var authenticator = CreateAuthenticator(connection, description);
110+
await authenticator.AuthenticateAsync(connection, description, cancellationToken).ConfigureAwait(false);
111+
}
72112

73-
var authenticator = CreateAuthenticator(description);
74-
return authenticator.AuthenticateAsync(connection, description, cancellationToken);
113+
114+
/// <inheritdoc/>
115+
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
116+
{
117+
return isMasterCommand.Merge(CreateSaslSupportedMechsRequest(_credential.Source, _credential.Username));
75118
}
76119

77-
private IAuthenticator CreateAuthenticator(ConnectionDescription description)
120+
private static BsonDocument CreateSaslSupportedMechsRequest(string authenticationDatabaseName, string userName)
78121
{
79-
if (Feature.ScramSha1Authentication.IsSupported(description.ServerVersion))
80-
{
81-
return new ScramSha1Authenticator(_credential, _randomStringGenerator);
82-
}
83-
else
122+
return new BsonDocument {{"saslSupportedMechs", $"{authenticationDatabaseName}.{userName}"}};
123+
}
124+
125+
// see https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst#defaults
126+
private IAuthenticator CreateAuthenticator(IConnection connection, ConnectionDescription description)
127+
{
128+
// If a saslSupportedMechs field was present in the isMaster results for mechanism negotiation,
129+
// then it MUST be inspected to select a default mechanism.
130+
if (description.IsMasterResult.HasSaslSupportedMechs)
84131
{
85-
return new MongoDBCRAuthenticator(_credential);
132+
// If SCRAM-SHA-256 is present in the list of mechanisms, then it MUST be used as the default;
133+
// otherwise, SCRAM-SHA-1 MUST be used as the default, regardless of whether SCRAM-SHA-1 is in the list.
134+
return description.IsMasterResult.SaslSupportedMechs.Contains("SCRAM-SHA-256")
135+
? (IAuthenticator) new ScramSha256Authenticator(_credential, _randomStringGenerator)
136+
: new ScramSha1Authenticator(_credential, _randomStringGenerator);
86137
}
138+
// If saslSupportedMechs is not present in the isMaster results for mechanism negotiation, then SCRAM-SHA-1
139+
// MUST be used when talking to servers >= 3.0. Prior to server 3.0, MONGODB-CR MUST be used.
140+
return Feature.ScramSha1Authentication.IsSupported(description.ServerVersion)
141+
? (IAuthenticator) new ScramSha1Authenticator(_credential, _randomStringGenerator)
142+
: new MongoDBCRAuthenticator(_credential);
87143
}
88144
}
89145
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.Threading;
1818
using System.Threading.Tasks;
19+
using MongoDB.Bson;
1920
using MongoDB.Driver.Core.Connections;
2021

2122
namespace MongoDB.Driver.Core.Authentication
@@ -49,5 +50,12 @@ public interface IAuthenticator
4950
/// <param name="cancellationToken">The cancellation token.</param>
5051
/// <returns>A Task.</returns>
5152
Task AuthenticateAsync(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken);
53+
54+
/// <summary>
55+
/// Optionally customizes isMaster command.
56+
/// </summary>
57+
/// <param name="isMasterCommand">Initial isMaster command.</param>
58+
/// <returns>Optionally mutated isMaster command.</returns>
59+
BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand);
5260
}
5361
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
104104
}
105105
}
106106

107+
/// <inheritdoc/>
108+
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
109+
{
110+
return isMasterCommand;
111+
}
112+
107113
// private methods
108114
private CommandWireProtocol<BsonDocument> CreateAuthenticateProtocol(BsonDocument getNonceReply)
109115
{

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
9898
}
9999
}
100100

101+
/// <inheritdoc/>
102+
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
103+
{
104+
return isMasterCommand;
105+
}
106+
101107
// private methods
102108
private CommandWireProtocol<BsonDocument> CreateAuthenticateProtocol()
103109
{

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
129129
}
130130
}
131131

132+
/// <inheritdoc/>
133+
public BsonDocument CustomizeInitialIsMasterCommand(BsonDocument isMasterCommand)
134+
{
135+
return isMasterCommand;
136+
}
137+
132138
private CommandWireProtocol<BsonDocument> CreateCommandProtocol(BsonDocument command)
133139
{
134140
return new CommandWireProtocol<BsonDocument>(
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* Copyright 2018–present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using System.IO;
18+
using System.Text;
19+
20+
namespace MongoDB.Driver.Core.Authentication
21+
{
22+
/// <summary>
23+
/// Per RFC5802: https://tools.ietf.org/html/rfc5802
24+
/// "SCRAM is a SASL mechanism whose client response and server challenge
25+
/// messages are text-based messages containing one or more attribute-
26+
/// value pairs separated by commas. Each attribute has a one-letter
27+
/// name."
28+
/// </summary>
29+
internal static class SaslMapParser
30+
{
31+
private const int EOF = -1;
32+
33+
public static IDictionary<char, string> Parse(string text)
34+
{
35+
IDictionary<char, string> dict = new Dictionary<char, string>();
36+
37+
using (var reader = new StringReader(text))
38+
{
39+
while (reader.Peek() != EOF)
40+
{
41+
dict.Add(ReadKeyValue(reader));
42+
if (reader.Peek() == ',')
43+
{
44+
Read(reader, ',');
45+
}
46+
}
47+
}
48+
49+
return dict;
50+
}
51+
52+
private static KeyValuePair<char, string> ReadKeyValue(TextReader reader)
53+
{
54+
var key = ReadKey(reader);
55+
Read(reader, '=');
56+
var value = ReadValue(reader);
57+
return new KeyValuePair<char, string>(key, value);
58+
}
59+
60+
private static char ReadKey(TextReader reader)
61+
{
62+
// keys are of length 1.
63+
return (char)reader.Read();
64+
}
65+
66+
private static void Read(TextReader reader, char expected)
67+
{
68+
var ch = (char)reader.Read();
69+
if (ch != expected)
70+
{
71+
throw new IOException(string.Format("Expected {0} but found {1}.", expected, ch));
72+
}
73+
}
74+
75+
private static string ReadValue(TextReader reader)
76+
{
77+
var sb = new StringBuilder();
78+
var ch = reader.Peek();
79+
while (ch != ',' && ch != EOF)
80+
{
81+
sb.Append((char)reader.Read());
82+
ch = reader.Peek();
83+
}
84+
85+
return sb.ToString();
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)