Skip to content

Commit 0bb467a

Browse files
CSHARP-4273: Cache AWS Credentials Where Possible.
1 parent 1c4c52b commit 0bb467a

File tree

11 files changed

+383
-43
lines changed

11 files changed

+383
-43
lines changed

evergreen/evergreen.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ functions:
514514
cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh"
515515
MONGODB_URI="mongodb://localhost"
516516
EOF
517+
export AWS_EC2_ENABLED=true
517518
PROJECT_DIRECTORY=${PROJECT_DIRECTORY} evergreen/run-mongodb-aws-test.sh
518519
519520
run-aws-auth-test-with-aws-credentials-as-environment-variables:
@@ -987,7 +988,7 @@ tasks:
987988
- func: run-aws-auth-test-with-aws-credentials-as-environment-variables
988989
- func: run-aws-auth-test-with-aws-credentials-and-session-token-as-environment-variables
989990
- func: run-aws-auth-test-with-aws-EC2-credentials
990-
# ECS test is skipped untill testing on Linux becomes possible
991+
# ECS test is skipped until testing on Linux becomes possible
991992

992993
- name: stable-api-tests-net472
993994
commands:

src/MongoDB.Driver.Core/Core/Authentication/External/AwsAuthenticationCredentialsProvider.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ namespace MongoDB.Driver.Core.Authentication.External
2525
{
2626
internal class AwsCredentials : IExternalCredentials
2727
{
28+
// credentials are considered expired when: Expiration - now > 5 mins
29+
private static readonly TimeSpan __overlapWhenExpired = TimeSpan.FromMinutes(5);
30+
2831
private readonly string _accessKeyId;
32+
private readonly DateTime? _expiration;
2933
private readonly SecureString _secretAccessKey;
3034
private readonly string _sessionToken;
3135

32-
public AwsCredentials(string accessKeyId, SecureString secretAccessKey, string sessionToken)
36+
public AwsCredentials(string accessKeyId, SecureString secretAccessKey, string sessionToken, string expiration)
3337
{
3438
_accessKeyId = Ensure.IsNotNull(accessKeyId, nameof(accessKeyId));
39+
_expiration = expiration != null ? DateTime.Parse(expiration) : null;
3540
_secretAccessKey = Ensure.IsNotNull(secretAccessKey, nameof(secretAccessKey));
3641
_sessionToken = sessionToken; // can be null
3742
}
3843

3944
public string AccessKeyId => _accessKeyId;
45+
public DateTime? Expiration => _expiration;
4046
public SecureString SecretAccessKey => _secretAccessKey;
4147
public string SessionToken => _sessionToken;
48+
public bool IsExpired => _expiration.HasValue ? (_expiration.Value - DateTime.UtcNow) < __overlapWhenExpired : false;
4249

4350
public BsonDocument GetKmsCredentials()
4451
=> new BsonDocument
@@ -93,7 +100,7 @@ private AwsCredentials CreateAwsCredentialsFromEnvironmentVariables()
93100
throw new InvalidOperationException($"When using AWS authentication if a session token is provided via environment variables then an access key ID and a secret access key must be provided also.");
94101
}
95102

96-
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken);
103+
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken, expiration: null);
97104
}
98105

99106
private async Task<AwsCredentials> CreateAwsCredentialsFromEcsResponseAsync(CancellationToken cancellationToken)
@@ -116,12 +123,23 @@ private async Task<AwsCredentials> CreateAwsCredentialsFromEc2ResponseAsync(Canc
116123

117124
private AwsCredentials CreateAwsCreadentialsFromAwsResponse(string awsResponse)
118125
{
126+
// Response template:
127+
//{
128+
// "Code": "Success",
129+
// "LastUpdated": "..",
130+
// "Type": "AWS-HMAC",
131+
// "AccessKeyId": "..",
132+
// "SecretAccessKey": "..",
133+
// "Token": "",
134+
// "Expiration": "YYYY-mm-ddThh:mm:ssZ"
135+
//}
119136
var parsedResponse = BsonDocument.Parse(awsResponse);
120137
var accessKeyId = parsedResponse.GetValue("AccessKeyId", null)?.AsString;
121138
var secretAccessKey = parsedResponse.GetValue("SecretAccessKey", null)?.AsString;
122139
var sessionToken = parsedResponse.GetValue("Token", null)?.AsString;
140+
var expiration = parsedResponse.GetValue("Expiration", null)?.AsString;
123141

124-
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken);
142+
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken, expiration);
125143
}
126144

127145
// nested types
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Copyright 2010-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.Threading;
17+
using System.Threading.Tasks;
18+
using MongoDB.Driver.Core.Misc;
19+
20+
namespace MongoDB.Driver.Core.Authentication.External
21+
{
22+
internal interface ICredentialsCache<TCredentials> where TCredentials : IExternalCredentials
23+
{
24+
TCredentials Credentials { get; }
25+
void Clear();
26+
}
27+
28+
internal class CacheableCredentialsProvider<TCredentials> : IExternalAuthenticationCredentialsProvider<TCredentials>, ICredentialsCache<TCredentials>
29+
where TCredentials : IExternalCredentials
30+
{
31+
private TCredentials _cachedCredentials;
32+
private readonly IExternalAuthenticationCredentialsProvider<TCredentials> _provider;
33+
34+
public CacheableCredentialsProvider(IExternalAuthenticationCredentialsProvider<TCredentials> provider)
35+
{
36+
_provider = Ensure.IsNotNull(provider, nameof(provider));
37+
}
38+
39+
public TCredentials Credentials => _cachedCredentials;
40+
41+
public TCredentials CreateCredentialsFromExternalSource(CancellationToken cancellationToken = default)
42+
{
43+
var cachedCredentials = _cachedCredentials;
44+
if (IsValidCache(cachedCredentials))
45+
{
46+
return cachedCredentials;
47+
}
48+
else
49+
{
50+
Clear();
51+
try
52+
{
53+
cachedCredentials = _provider.CreateCredentialsFromExternalSource(cancellationToken);
54+
_cachedCredentials = cachedCredentials;
55+
return cachedCredentials;
56+
}
57+
catch
58+
{
59+
Clear();
60+
throw;
61+
}
62+
}
63+
}
64+
65+
public async Task<TCredentials> CreateCredentialsFromExternalSourceAsync(CancellationToken cancellationToken = default)
66+
{
67+
var cachedCredentials = _cachedCredentials;
68+
if (IsValidCache(cachedCredentials))
69+
{
70+
return cachedCredentials;
71+
}
72+
else
73+
{
74+
Clear();
75+
try
76+
{
77+
cachedCredentials = await _provider.CreateCredentialsFromExternalSourceAsync(cancellationToken).ConfigureAwait(false);
78+
_cachedCredentials = cachedCredentials;
79+
return cachedCredentials;
80+
}
81+
catch
82+
{
83+
Clear();
84+
throw;
85+
}
86+
}
87+
}
88+
89+
// private methods
90+
private bool IsValidCache(TCredentials credentials) => credentials != null && !credentials.IsExpired;
91+
public void Clear() => _cachedCredentials = default;
92+
}
93+
}

src/MongoDB.Driver.Core/Core/Authentication/External/ExternalCredentialsAuthenticators.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.Net.Http;
1817
using MongoDB.Driver.Core.Misc;
1918

2019
namespace MongoDB.Driver.Core.Authentication.External
@@ -38,7 +37,7 @@ internal ExternalCredentialsAuthenticators() : this(new HttpClientHelper())
3837
internal ExternalCredentialsAuthenticators(HttpClientHelper httpClientHelper)
3938
{
4039
_httpClientHelper = Ensure.IsNotNull(httpClientHelper, nameof(httpClientHelper));
41-
_awsExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<AwsCredentials>>(() => new AwsAuthenticationCredentialsProvider(_httpClientHelper), isThreadSafe: true);
40+
_awsExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<AwsCredentials>>(() => new CacheableCredentialsProvider<AwsCredentials>(new AwsAuthenticationCredentialsProvider(_httpClientHelper)), isThreadSafe: true);
4241
_gcpExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<GcpCredentials>>(() => new GcpAuthenticationCredentialsProvider(_httpClientHelper), isThreadSafe: true);
4342
}
4443

src/MongoDB.Driver.Core/Core/Authentication/External/GcpAuthenticationCredentialsProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ internal class GcpCredentials : IExternalCredentials
3030

3131
public string AccessToken => _accessToken;
3232

33+
public DateTime? Expiration => null;
34+
35+
public bool IsExpired => false;
36+
3337
public BsonDocument GetKmsCredentials() => new BsonDocument("accessToken", _accessToken);
3438
}
3539

src/MongoDB.Driver.Core/Core/Authentication/External/IExternalCredentials.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using MongoDB.Bson;
1718

1819
namespace MongoDB.Driver.Core.Authentication.External
1920
{
2021
internal interface IExternalCredentials
2122
{
23+
DateTime? Expiration { get; }
24+
bool IsExpired { get; }
2225
BsonDocument GetKmsCredentials();
2326
}
2427
}

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

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System.Collections.Generic;
1818
using System.Linq;
1919
using System.Security;
20+
using System.Threading;
21+
using System.Threading.Tasks;
2022
using MongoDB.Bson;
2123
using MongoDB.Bson.Serialization;
2224
using MongoDB.Driver.Core.Authentication.External;
@@ -51,26 +53,28 @@ private static MongoAWSMechanism CreateMechanism(
5153
UsernamePasswordCredential credential,
5254
IEnumerable<KeyValuePair<string, string>> properties,
5355
IRandomByteGenerator randomByteGenerator,
56+
IExternalAuthenticationCredentialsProvider<AwsCredentials> externalAuthenticationCredentialsProvider,
5457
IClock clock)
5558
{
5659
if (credential.Source != "$external")
5760
{
5861
throw new ArgumentException("MONGODB-AWS authentication may only use the $external source.", nameof(credential));
5962
}
6063

61-
return CreateMechanism(credential.Username, credential.Password, properties, randomByteGenerator, clock);
64+
return CreateMechanism(credential.Username, credential.Password, properties, randomByteGenerator, externalAuthenticationCredentialsProvider, clock);
6265
}
6366

6467
private static MongoAWSMechanism CreateMechanism(
6568
string username,
6669
SecureString password,
6770
IEnumerable<KeyValuePair<string, string>> properties,
6871
IRandomByteGenerator randomByteGenerator,
72+
IExternalAuthenticationCredentialsProvider<AwsCredentials> externalAuthenticationCredentialsProvider,
6973
IClock clock)
7074
{
7175
var awsCredentials =
7276
CreateAwsCredentialsFromMongoCredentials(username, password, properties) ??
73-
ExternalCredentialsAuthenticators.Instance.Aws.CreateCredentialsFromExternalSource();
77+
externalAuthenticationCredentialsProvider.CreateCredentialsFromExternalSource();
7478

7579
return new MongoAWSMechanism(awsCredentials, randomByteGenerator, clock);
7680
}
@@ -97,7 +101,7 @@ private static AwsCredentials CreateAwsCredentialsFromMongoCredentials(string us
97101
throw new InvalidOperationException("When using MONGODB-AWS authentication if a session token is provided via settings then a username and password must be provided also.");
98102
}
99103

100-
return new AwsCredentials(accessKeyId: username, secretAccessKey: password, sessionToken);
104+
return new AwsCredentials(accessKeyId: username, secretAccessKey: password, sessionToken, expiration: null);
101105
}
102106

103107
private static string ExtractSessionTokenFromMechanismProperties(IEnumerable<KeyValuePair<string, string>> properties)
@@ -131,18 +135,9 @@ private static void ValidateMechanismProperties(IEnumerable<KeyValuePair<string,
131135
}
132136
#endregion
133137

134-
// constructors
135-
/// <summary>
136-
/// Initializes a new instance of the <see cref="MongoAWSAuthenticator"/> class.
137-
/// </summary>
138-
/// <param name="credential">The credentials.</param>
139-
/// <param name="properties">The properties.</param>
140-
[Obsolete("Use the newest overload instead.")]
141-
public MongoAWSAuthenticator(UsernamePasswordCredential credential, IEnumerable<KeyValuePair<string, string>> properties)
142-
: this(credential, properties, serverApi: null)
143-
{
144-
}
138+
private readonly ICredentialsCache<AwsCredentials> _credentialsCache;
145139

140+
// constructors
146141
/// <summary>
147142
/// Initializes a new instance of the <see cref="MongoAWSAuthenticator"/> class.
148143
/// </summary>
@@ -153,18 +148,13 @@ public MongoAWSAuthenticator(
153148
UsernamePasswordCredential credential,
154149
IEnumerable<KeyValuePair<string, string>> properties,
155150
ServerApi serverApi)
156-
: this(credential, properties, new DefaultRandomByteGenerator(), SystemClock.Instance, serverApi)
157-
{
158-
}
159-
160-
/// <summary>
161-
/// Initializes a new instance of the <see cref="MongoAWSAuthenticator"/> class.
162-
/// </summary>
163-
/// <param name="username">The username.</param>
164-
/// <param name="properties">The properties.</param>
165-
[Obsolete("Use the newest overload instead.")]
166-
public MongoAWSAuthenticator(string username, IEnumerable<KeyValuePair<string, string>> properties)
167-
: this(username, properties, serverApi: null)
151+
: this(
152+
credential,
153+
properties,
154+
new DefaultRandomByteGenerator(),
155+
ExternalCredentialsAuthenticators.Instance.Aws,
156+
SystemClock.Instance,
157+
serverApi)
168158
{
169159
}
170160

@@ -178,28 +168,38 @@ public MongoAWSAuthenticator(
178168
string username,
179169
IEnumerable<KeyValuePair<string, string>> properties,
180170
ServerApi serverApi)
181-
: this(username, properties, new DefaultRandomByteGenerator(), SystemClock.Instance, serverApi)
171+
: this(
172+
username,
173+
properties,
174+
new DefaultRandomByteGenerator(),
175+
ExternalCredentialsAuthenticators.Instance.Aws,
176+
SystemClock.Instance,
177+
serverApi)
182178
{
183179
}
184180

185181
internal MongoAWSAuthenticator(
186182
UsernamePasswordCredential credential,
187183
IEnumerable<KeyValuePair<string, string>> properties,
188184
IRandomByteGenerator randomByteGenerator,
185+
IExternalAuthenticationCredentialsProvider<AwsCredentials> externalAuthenticationCredentialsProvider,
189186
IClock clock,
190187
ServerApi serverApi)
191-
: base(CreateMechanism(credential, properties, randomByteGenerator, clock), serverApi)
188+
: base(CreateMechanism(credential, properties, randomByteGenerator, externalAuthenticationCredentialsProvider, clock), serverApi)
192189
{
190+
_credentialsCache = externalAuthenticationCredentialsProvider as ICredentialsCache<AwsCredentials>; // can be null
193191
}
194192

195193
internal MongoAWSAuthenticator(
196194
string username,
197195
IEnumerable<KeyValuePair<string, string>> properties,
198196
IRandomByteGenerator randomByteGenerator,
197+
IExternalAuthenticationCredentialsProvider<AwsCredentials> externalAuthenticationCredentialsProvider,
199198
IClock clock,
200199
ServerApi serverApi)
201-
: base(CreateMechanism(username, null, properties, randomByteGenerator, clock), serverApi)
200+
: base(CreateMechanism(username, null, properties, randomByteGenerator, externalAuthenticationCredentialsProvider, clock), serverApi)
202201
{
202+
_credentialsCache = externalAuthenticationCredentialsProvider as ICredentialsCache<AwsCredentials>; // can be null
203203
}
204204

205205
/// <inheritdoc/>
@@ -208,6 +208,34 @@ public override string DatabaseName
208208
get { return "$external"; }
209209
}
210210

211+
/// <inheritdoc/>
212+
public override void Authenticate(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken)
213+
{
214+
try
215+
{
216+
base.Authenticate(connection, description, cancellationToken);
217+
}
218+
catch
219+
{
220+
_credentialsCache?.Clear();
221+
throw;
222+
}
223+
}
224+
225+
/// <inheritdoc/>
226+
public override async Task AuthenticateAsync(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken)
227+
{
228+
try
229+
{
230+
await base.AuthenticateAsync(connection, description, cancellationToken).ConfigureAwait(false);
231+
}
232+
catch
233+
{
234+
_credentialsCache?.Clear();
235+
throw;
236+
}
237+
}
238+
211239
// nested classes
212240
private class MongoAWSMechanism : ISaslMechanism
213241
{

0 commit comments

Comments
 (0)