Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSHARP-4448: Implement OIDC SASL mechanism #1259

Merged
merged 2 commits into from
Apr 11, 2024

Conversation

sanych-sun
Copy link
Member

@sanych-sun sanych-sun commented Feb 2, 2024

Current PR contains following changes:

  1. Added async counterparts for Initialize of SaslMechanism
  2. Implemented OIDC authenticator
  3. Implemented spec tests (will need to add read.me on how to run the tests locally)
  4. Implement Prose spec tests.
  5. Configure EG scripts/variants.

@sanych-sun sanych-sun requested a review from a team as a code owner February 2, 2024 18:15
@sanych-sun sanych-sun requested review from adelinowona, JamesKovacs and BorisDog and removed request for a team and adelinowona February 2, 2024 18:15
@@ -140,5 +142,15 @@ private void AssertIsError(Exception actualException, bool expectedIsError)

actualException.Should().NotBeNull();
}

private static Exception UnwrapConnectionException(Exception ex)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to add this, because unified tests trying to check the returned by server error code, but MongoDB C# driver wrap the original exception by MongoConnectionException in some particular cases.


public void ClearCache()
{
_cachedCredentials = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might need synchronization. Or maybe even better, versioning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


var response = _wrappedCallbackProvider.GetResponse(parameters, cancellationToken);
credentials = new OidcCredentials(response.AccessToken, response.ExpiresIn, _clock);
_cachedCredentials = credentials;
Copy link
Contributor

@BorisDog BorisDog Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the following execution possible?

conn1/conn2 use same OidcCallbackAdapter
conn1/conn2 on Auth exception:

conn1: ClearCache
conn1: start GetCreds
conn1: _cachedCredentials = newCreds_conn1

conn2: ClearCache (_cachedCredentials == null)
conn1: finish GetCreds // _cachedCredentials == null

conn2: start GetCreds // _cachedCredentials == null
conn2: cachedCredentials = newCreds_conn2

newCreds_conn1 is now invalid (?), if so go to line: "conn1: ClearCache", which results in life-lock?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be a problem anymore, as code was improved to invalidate only own token.

src/MongoDB.Driver/MongoCredential.cs Show resolved Hide resolved
src/MongoDB.Driver/MongoCredential.cs Outdated Show resolved Hide resolved
src/MongoDB.Driver/MongoCredential.cs Outdated Show resolved Hide resolved
src/MongoDB.Driver/MongoCredential.cs Show resolved Hide resolved

[Theory]
[MemberData(nameof(ValidConfigurationTestCases))]
public void Should_accept_valid_arguments(string principalName, IReadOnlyDictionary<string, object> mechanismProperties)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the convection is to have ctor_ in test name. We are testing the ctor afterall.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[Theory]
[MemberData(nameof(InvalidConfigurationTestCases))]
public void Should_throw_on_invalid_arguments(string principalName, IReadOnlyDictionary<string, object> mechanismProperties)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding and validating string exceptionMessage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added paramName validation.

@@ -338,7 +338,7 @@ public void Constructor_should_throw_an_ArgumentNullException_when_credential_is
{
// We must call CustomizeInitialHelloCommand so that the authenticator thinks its started to speculatively
// authenticate
helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } });
helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }, default);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a good idea to have default value to cancelation token CancellationToken cancellationToken = default like in rest of the API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other IAuthenticator members does not have default value for cancellationToken, would prefer to keep it as is for consistancy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our authentication API is really an internal implementation detail. We can require CancellationToken instances be specified IMHO.

@@ -37,7 +37,7 @@ public class ClusterBuilderTests
public void CreateServerMonitorFactory_should_return_expected_result(int connectTimeoutMilliseconds, int heartbeatTimeoutMilliseconds, int expectedServerMonitorConnectTimeoutMilliseconds, int expectedServerMonitorSocketTimeoutMilliseconds)
{
var connectTimeout = TimeSpan.FromMilliseconds(connectTimeoutMilliseconds);
var authenticatorFactories = new[] { new AuthenticatorFactory(() => new DefaultAuthenticator(new UsernamePasswordCredential("source", "username", "password"), serverApi: null)) };
var authenticatorFactories = new[] { new AuthenticatorFactory(c => new DefaultAuthenticator(new UsernamePasswordCredential("source", "username", "password"), serverApi: null)) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: for unused param could just use _, here and everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

var db = client.GetDatabase(DatabaseName);
var collection = db.GetCollection<BsonDocument>(CollectionName);

var tasks = Enumerable.Range(0, 10).Select(_ => Task.Run(async () =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need to use ThreadingUtilities.ExecuteOnNewThreads for this.
Both for more deterministic testing, and for running actual 10 parallel threads.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


await Task.WhenAll(tasks);

if (async)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could extract this to a helper method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -161,6 +162,11 @@ public static DisposableMongoClient CreateDisposableClient(Action<ClusterBuilder
clientSettings.ServerApi = CoreTestConfiguration.ServerApi;
clientSettingsConfigurator?.Invoke(clientSettings);

if (clientSettings.Credential.Mechanism == MongoOidcAuthenticator.MechanismName)
{
OidcCallbackAdapterCachingFactory.Instance.Reset();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will collide with the future task of tests parallelization and creds caching tests. Not sure what's the best solution though.

Copy link
Member Author

@sanych-sun sanych-sun Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we will. We have to provide a way to substitute the driver's components and replace the IOidcCallbackAdapterFactory in this case, so it will produce new Adapter for each of test. Probably ClientSettings could be nice point to host components.

var exception = Record.Exception(() =>
new OidcConfiguration(__endPoint, principalName, mechanismProperties, __buildInProvidersMock));

exception.Should().NotBeNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need proper tests both for equality and cashingFactory.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// <param name="parameters">The information used by callbacks to authenticate with the Identity Provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>OIDC callback response</returns>
OidcCallbackResponse GetOidcAccessToken(OidcCallbackParameters parameters, CancellationToken cancellationToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do we want to return OidcAccessToken, or still Response?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

Copy link
Contributor

@JamesKovacs JamesKovacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great. See inline feedback. Biggest question is whether we can acquire and cache auth tokens per cluster or if we must do it per endpoint. Right now the code implements per-endpoint.

src/MongoDB.Driver/MongoCredential.cs Outdated Show resolved Hide resolved
@@ -338,7 +338,7 @@ public void Constructor_should_throw_an_ArgumentNullException_when_credential_is
{
// We must call CustomizeInitialHelloCommand so that the authenticator thinks its started to speculatively
// authenticate
helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } });
helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }, default);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our authentication API is really an internal implementation detail. We can require CancellationToken instances be specified IMHO.

Copy link
Contributor

@JamesKovacs JamesKovacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

return null;
}

return CreateNoTransitionClientLastSaslStep(cachedCredentials);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to update _usedCredentials here as well?
If not, consider putting a comment explaining that. If yes, could just do it in CreateNoTransitionClientLastSaslStep.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Added.

@sanych-sun sanych-sun requested a review from BorisDog March 1, 2024 00:40
Copy link
Contributor

@BorisDog BorisDog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@sanych-sun sanych-sun force-pushed the csharp4448-2 branch 2 times, most recently from 49a229f to 901d38f Compare March 5, 2024 21:45
@sanych-sun sanych-sun requested a review from JamesKovacs April 4, 2024 23:58
Copy link
Contributor

@JamesKovacs JamesKovacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor rewording of error message but otherwise LGTM.

if (bytesReceivedFromServer?.Length > 0)
{
// should not be reached
throw new InvalidOperationException("Not all authentication response has been handled.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all authentication response has been handled.

Received an additional {bytesReceivedFromServer} bytes from the server in last SASL response that was not expected.

@@ -80,7 +80,7 @@
},
{
"description": "should accept generic mechanism property (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are related to https://jira.mongodb.org/browse/CSHARP-3796, which we haven't implemented yet. Do the Kerberos (GSSAPI) forward/forwardAndReverse tests actually pass?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in Slack. MONGODB_URIs with CANONICALIZE_HOST_NAME present are skipped.

@sanych-sun sanych-sun changed the title [WIP] CSHARP-4448: Implement OIDC SASL mechanism CSHARP-4448: Implement OIDC SASL mechanism Apr 11, 2024
@sanych-sun sanych-sun merged commit 6817795 into mongodb:master Apr 11, 2024
26 of 28 checks passed
@sanych-sun sanych-sun deleted the csharp4448-2 branch April 11, 2024 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants