Skip to content

Commit aff12a2

Browse files
committed
Implement OIDC SASL mechanism
JAVA-4757
1 parent 9490ea5 commit aff12a2

File tree

17 files changed

+1369
-104
lines changed

17 files changed

+1369
-104
lines changed

driver-core/src/main/com/mongodb/AuthenticationMechanism.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public enum AuthenticationMechanism {
3737
*/
3838
MONGODB_AWS("MONGODB-AWS"),
3939

40+
/**
41+
* The MONGODB-OIDC mechanism.
42+
* @since 5.0
43+
* @mongodb.server.release TODO
44+
*/
45+
MONGODB_OIDC("MONGODB-OIDC"),
46+
4047
/**
4148
* The MongoDB X.509 mechanism. This mechanism is available only with client certificates over SSL.
4249
*/

driver-core/src/main/com/mongodb/ConnectionString.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.Set;
4040
import java.util.concurrent.TimeUnit;
4141

42+
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential;
4243
import static java.lang.String.format;
4344
import static java.util.Arrays.asList;
4445
import static java.util.Collections.singletonList;
@@ -905,6 +906,10 @@ private MongoCredential createMongoCredentialWithMechanism(final AuthenticationM
905906
case MONGODB_AWS:
906907
credential = MongoCredential.createAwsCredential(userName, password);
907908
break;
909+
case MONGODB_OIDC:
910+
validateCreateOidcCredential(password);
911+
credential = MongoCredential.createOidcCredentialInternal(userName);
912+
break;
908913
default:
909914
throw new UnsupportedOperationException(format("The connection string contains an invalid authentication mechanism'. "
910915
+ "'%s' is not a supported authentication mechanism",

driver-core/src/main/com/mongodb/MongoCredential.java

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,27 @@
1919
import com.mongodb.annotations.Beta;
2020
import com.mongodb.annotations.Immutable;
2121
import com.mongodb.lang.Nullable;
22+
import org.bson.BsonDocument;
23+
import org.bson.BsonString;
24+
import org.bson.conversions.Bson;
2225

2326
import java.util.Arrays;
2427
import java.util.Collections;
2528
import java.util.HashMap;
29+
import java.util.List;
2630
import java.util.Map;
2731
import java.util.Objects;
32+
import java.util.stream.Collectors;
2833

2934
import static com.mongodb.AuthenticationMechanism.GSSAPI;
3035
import static com.mongodb.AuthenticationMechanism.MONGODB_AWS;
36+
import static com.mongodb.AuthenticationMechanism.MONGODB_OIDC;
3137
import static com.mongodb.AuthenticationMechanism.MONGODB_X509;
3238
import static com.mongodb.AuthenticationMechanism.PLAIN;
3339
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1;
3440
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_256;
3541
import static com.mongodb.assertions.Assertions.notNull;
42+
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateOidcCredentialConstruction;
3643

3744
/**
3845
* Represents credentials to authenticate to a mongo server,as well as the source of the credentials and the authentication mechanism to
@@ -179,6 +186,21 @@ public final class MongoCredential {
179186
@Beta(Beta.Reason.CLIENT)
180187
public static final String AWS_CREDENTIAL_PROVIDER_KEY = "AWS_CREDENTIAL_PROVIDER";
181188

189+
/**
190+
*
191+
*/
192+
public static final String PROVIDER_NAME = "PROVIDER_NAME";
193+
194+
/**
195+
*
196+
*/
197+
public static final String REQUEST_TOKEN_CALLBACK = "REQUEST_TOKEN_CALLBACK";
198+
199+
/**
200+
*
201+
*/
202+
public static final String REFRESH_TOKEN_CALLBACK = "REFRESH_TOKEN_CALLBACK";
203+
182204
/**
183205
* Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the
184206
* version of the server that the client is authenticating to.
@@ -327,6 +349,45 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam
327349
return new MongoCredential(MONGODB_AWS, userName, "$external", password);
328350
}
329351

352+
/**
353+
*
354+
* @param userName
355+
* @param request
356+
* @param refresh
357+
* @return
358+
*/
359+
public static MongoCredential createOidcCredential(
360+
@Nullable final String userName,
361+
final RequestCallback request,
362+
@Nullable final RefreshCallback refresh) {
363+
MongoCredential mongoCredential = createOidcCredentialInternal(userName)
364+
.withMechanismProperty(REQUEST_TOKEN_CALLBACK, request);
365+
if (refresh != null) {
366+
mongoCredential = mongoCredential
367+
.withMechanismProperty(REFRESH_TOKEN_CALLBACK, refresh);
368+
}
369+
return mongoCredential;
370+
}
371+
372+
/**
373+
*
374+
* @param providerName
375+
* @return
376+
*/
377+
public static MongoCredential createOidcCredential(final String providerName) {
378+
return createOidcCredentialInternal(null)
379+
.withMechanismProperty(PROVIDER_NAME, providerName);
380+
}
381+
382+
/**
383+
*
384+
* @param userName
385+
* @return
386+
*/
387+
static MongoCredential createOidcCredentialInternal(@Nullable final String userName) {
388+
return new MongoCredential(MONGODB_OIDC, userName, "$external", null);
389+
}
390+
330391
/**
331392
* Creates a new MongoCredential as a copy of this instance, with the specified mechanism property added.
332393
*
@@ -377,7 +438,11 @@ public MongoCredential withMechanism(final AuthenticationMechanism mechanism) {
377438
@Nullable final char[] password,
378439
final Map<String, Object> mechanismProperties) {
379440

380-
if (userName == null && !Arrays.asList(MONGODB_X509, MONGODB_AWS).contains(mechanism)) {
441+
if (mechanism == MONGODB_OIDC) {
442+
validateOidcCredentialConstruction(source, mechanismProperties);
443+
}
444+
445+
if (userName == null && !Arrays.asList(MONGODB_X509, MONGODB_AWS, MONGODB_OIDC).contains(mechanism)) {
381446
throw new IllegalArgumentException("username can not be null");
382447
}
383448

@@ -552,4 +617,135 @@ public String toString() {
552617
+ ", mechanismProperties=<hidden>"
553618
+ '}';
554619
}
620+
621+
// TODO-OIDC
622+
public interface RequestCallback {
623+
OidcTokens callback(
624+
@Nullable String principalName,
625+
IdpServerInfo serverInfo,
626+
int timeoutSeconds);
627+
}
628+
629+
public interface RefreshCallback {
630+
OidcTokens callback(
631+
@Nullable String principalName,
632+
IdpServerInfo serverInfo,
633+
OidcTokens tokens,
634+
int timeoutSeconds);
635+
}
636+
637+
/**
638+
* Server's reply to clientStep1.
639+
* <p>
640+
* IDP's configuration will be returned in serverStep1 to instruct the client
641+
* on how to acquire an Access Token.
642+
*/
643+
public static class IdpServerInfo {
644+
private final String issuer;
645+
private final String clientId;
646+
@Nullable
647+
private final List<String> requestScopes;
648+
649+
/**
650+
* URL which describes the Authorization Server. This identifier should
651+
* be the iss of provided access tokens, and be viable for RFC8414
652+
* metadata discovery and RFC9207 identification.
653+
*/
654+
public String getIssuer() {
655+
return issuer;
656+
}
657+
658+
/**
659+
* Unique client ID for this OIDC client
660+
*/
661+
public String getClientId() {
662+
return clientId;
663+
}
664+
665+
/**
666+
* Additional scopes to request from IDP
667+
*/
668+
@Nullable
669+
public List<String> getRequestScopes() {
670+
return requestScopes;
671+
}
672+
673+
public IdpServerInfo(final Bson bson) {
674+
// TODO-OIDC move to internal?
675+
BsonDocument c = bson.toBsonDocument();
676+
this.issuer = getString(c, "issuer");
677+
this.clientId = getString(c, "clientId");
678+
this.requestScopes = getStringArray(c, "requestScopes");
679+
}
680+
681+
@Nullable
682+
private static String getString(final BsonDocument document, final String key) {
683+
if (!document.containsKey(key) || document.isString(key)) {
684+
return null;
685+
}
686+
return document.getString(key).getValue();
687+
}
688+
689+
@Nullable
690+
private static List<String> getStringArray(final BsonDocument document, final String key) {
691+
if (!document.containsKey(key) || document.isString(key)) {
692+
return null;
693+
}
694+
List<String> result = document.getArray(key).getValues().stream()
695+
// ignore non-string values from server, rather than error
696+
.filter(v -> v.isString())
697+
.map(v -> v.asString().getValue())
698+
.collect(Collectors.toList());
699+
return Collections.unmodifiableList(result);
700+
}
701+
}
702+
703+
/**
704+
* The result of a token request
705+
*/
706+
public static class OidcTokens {
707+
708+
private final String accessToken;
709+
710+
private final Integer expiresInSeconds;
711+
712+
private final String refreshToken;
713+
714+
/**
715+
* The OIDC access token
716+
*/
717+
public String getAccessToken() {
718+
return accessToken;
719+
}
720+
721+
/**
722+
* The expiration time in seconds from the current time
723+
*/
724+
@Nullable
725+
public Integer getExpiresInSeconds() {
726+
return expiresInSeconds;
727+
}
728+
729+
/**
730+
* The OIDC refresh token
731+
*/
732+
@Nullable
733+
public String getRefreshToken() {
734+
return refreshToken;
735+
}
736+
737+
public OidcTokens(
738+
final String accessToken,
739+
@Nullable final Integer expiresInSeconds,
740+
@Nullable final String refreshToken) {
741+
this.accessToken = accessToken;
742+
this.expiresInSeconds = expiresInSeconds;
743+
this.refreshToken = refreshToken;
744+
}
745+
746+
public final BsonDocument toBsonDocument() { // TODO-OIDC move to internal?
747+
return new BsonDocument()
748+
.append("jwt", new BsonString(getAccessToken()));
749+
}
750+
}
555751
}

driver-core/src/main/com/mongodb/internal/connection/AwsAuthenticator.java

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.mongodb.internal.connection;
1818

19-
import com.mongodb.AuthenticationMechanism;
2019
import com.mongodb.AwsCredential;
2120
import com.mongodb.MongoClientException;
2221
import com.mongodb.MongoCredential;
@@ -77,27 +76,12 @@ protected SaslClient createSaslClient(final ServerAddress serverAddress) {
7776
return new AwsSaslClient(getMongoCredential());
7877
}
7978

80-
private static class AwsSaslClient implements SaslClient {
81-
private final MongoCredential credential;
79+
private static class AwsSaslClient extends SaslClientImpl {
8280
private final byte[] clientNonce = new byte[RANDOM_LENGTH];
8381
private int step = -1;
8482

8583
AwsSaslClient(final MongoCredential credential) {
86-
this.credential = credential;
87-
}
88-
89-
@Override
90-
public String getMechanismName() {
91-
AuthenticationMechanism authMechanism = credential.getAuthenticationMechanism();
92-
if (authMechanism == null) {
93-
throw new IllegalArgumentException("Authentication mechanism cannot be null");
94-
}
95-
return authMechanism.getMechanismName();
96-
}
97-
98-
@Override
99-
public boolean hasInitialResponse() {
100-
return true;
84+
super(credential);
10185
}
10286

10387
@Override
@@ -117,26 +101,6 @@ public boolean isComplete() {
117101
return step == 1;
118102
}
119103

120-
@Override
121-
public byte[] unwrap(final byte[] bytes, final int i, final int i1) {
122-
throw new UnsupportedOperationException("Not implemented yet!");
123-
}
124-
125-
@Override
126-
public byte[] wrap(final byte[] bytes, final int i, final int i1) {
127-
throw new UnsupportedOperationException("Not implemented yet!");
128-
}
129-
130-
@Override
131-
public Object getNegotiatedProperty(final String s) {
132-
throw new UnsupportedOperationException("Not implemented yet!");
133-
}
134-
135-
@Override
136-
public void dispose() {
137-
// nothing to do
138-
}
139-
140104
private byte[] computeClientFirstMessage() {
141105
new SecureRandom().nextBytes(this.clientNonce);
142106

@@ -184,16 +148,16 @@ private byte[] computeClientFinalMessage(final byte[] serverFirst) throws SaslEx
184148

185149
private AwsCredential createAwsCredential() {
186150
AwsCredential awsCredential;
187-
if (credential.getUserName() != null) {
188-
if (credential.getPassword() == null) {
151+
if (getCredential().getUserName() != null) {
152+
if (getCredential().getPassword() == null) {
189153
throw new MongoClientException("secretAccessKey is required for AWS credential");
190154
}
191-
awsCredential = new AwsCredential(assertNotNull(credential.getUserName()),
192-
new String(assertNotNull(credential.getPassword())),
193-
credential.getMechanismProperty(AWS_SESSION_TOKEN_KEY, null));
194-
} else if (credential.getMechanismProperty(AWS_CREDENTIAL_PROVIDER_KEY, null) != null) {
155+
awsCredential = new AwsCredential(assertNotNull(getCredential().getUserName()),
156+
new String(assertNotNull(getCredential().getPassword())),
157+
getCredential().getMechanismProperty(AWS_SESSION_TOKEN_KEY, null));
158+
} else if (getCredential().getMechanismProperty(AWS_CREDENTIAL_PROVIDER_KEY, null) != null) {
195159
Supplier<AwsCredential> awsCredentialSupplier = assertNotNull(
196-
credential.getMechanismProperty(AWS_CREDENTIAL_PROVIDER_KEY, null));
160+
getCredential().getMechanismProperty(AWS_CREDENTIAL_PROVIDER_KEY, null));
197161
awsCredential = awsCredentialSupplier.get();
198162
if (awsCredential == null) {
199163
throw new MongoClientException("AWS_CREDENTIAL_PROVIDER_KEY must return an AwsCredential instance");

driver-core/src/main/com/mongodb/internal/connection/InternalConnectionInitializer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.mongodb.internal.connection;
1818

19+
import com.mongodb.connection.ConnectionDescription;
1920
import com.mongodb.internal.async.SingleResultCallback;
2021

2122
interface InternalConnectionInitializer {
@@ -30,4 +31,13 @@ void startHandshakeAsync(InternalConnection internalConnection,
3031

3132
void finishHandshakeAsync(InternalConnection internalConnection, InternalConnectionInitializationDescription description,
3233
SingleResultCallback<InternalConnectionInitializationDescription> callback);
34+
35+
void reauthenticate(
36+
InternalStreamConnection internalStreamConnection,
37+
ConnectionDescription connectionDescription);
38+
39+
void reauthenticateAsync(
40+
InternalStreamConnection internalStreamConnection,
41+
ConnectionDescription connectionDescription,
42+
SingleResultCallback<Void> callback);
3343
}

0 commit comments

Comments
 (0)