Skip to content

Commit

Permalink
Add Human OIDC Workflow (#1316)
Browse files Browse the repository at this point in the history
* Add human workflow

* Apply suggestions from code review

Co-authored-by: Valentin Kovalenko <valentin.kovalenko@mongodb.com>

* Add expiresIn, address PR comments

* PR fixes

* Fix compilation

---------

Co-authored-by: Valentin Kovalenko <valentin.kovalenko@mongodb.com>
  • Loading branch information
katcharov and stIncMale committed Mar 4, 2024
1 parent d460444 commit 45d1c59
Show file tree
Hide file tree
Showing 6 changed files with 845 additions and 78 deletions.
11 changes: 11 additions & 0 deletions driver-core/src/main/com/mongodb/ConnectionString.java
Expand Up @@ -44,7 +44,10 @@
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential;
import static java.lang.String.format;
import static java.util.Arrays.asList;
Expand Down Expand Up @@ -272,6 +275,9 @@ public class ConnectionString {
private static final Set<String> ALLOWED_OPTIONS_IN_TXT_RECORD =
new HashSet<>(asList("authsource", "replicaset", "loadbalanced"));
private static final Logger LOGGER = Loggers.getLogger("uri");
private static final List<String> MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING = Stream.of(ALLOWED_HOSTS_KEY)
.map(k -> k.toLowerCase())
.collect(Collectors.toList());

private final MongoCredential credential;
private final boolean isSrvProtocol;
Expand Down Expand Up @@ -902,6 +908,11 @@ private MongoCredential createCredentials(final Map<String, List<String>> option
}
String key = mechanismPropertyKeyValue[0].trim().toLowerCase();
String value = mechanismPropertyKeyValue[1].trim();
if (MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING.contains(key)) {
throw new IllegalArgumentException(format("The connection string contains disallowed mechanism properties. "
+ "'%s' must be set on the credential programmatically.", key));
}

if (key.equals("canonicalize_host_name")) {
credential = credential.withMechanismProperty(key, Boolean.valueOf(value));
} else {
Expand Down
135 changes: 125 additions & 10 deletions driver-core/src/main/com/mongodb/MongoCredential.java
Expand Up @@ -25,6 +25,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

Expand Down Expand Up @@ -187,7 +188,8 @@ public final class MongoCredential {
* The provider name. The value must be a string.
* <p>
* If this is provided,
* {@link MongoCredential#OIDC_CALLBACK_KEY}
* {@link MongoCredential#OIDC_CALLBACK_KEY} and
* {@link MongoCredential#OIDC_HUMAN_CALLBACK_KEY}
* must not be provided.
*
* @see #createOidcCredential(String)
Expand All @@ -197,17 +199,60 @@ public final class MongoCredential {

/**
* This callback is invoked when the OIDC-based authenticator requests
* tokens from the identity provider. The type of the value must be
* {@link OidcRequestCallback}.
* a token. The type of the value must be {@link OidcCallback}.
* {@link IdpInfo} will not be supplied to the callback,
* and a {@linkplain OidcCallbackResult#getRefreshToken() refresh token}
* must not be returned by the callback.
* <p>
* If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY}
* and {@link MongoCredential#OIDC_HUMAN_CALLBACK_KEY}
* must not be provided.
*
* @see #createOidcCredential(String)
* @since 4.10
*/
public static final String OIDC_CALLBACK_KEY = "OIDC_CALLBACK";

/**
* This callback is invoked when the OIDC-based authenticator requests
* a token from the identity provider (IDP) using the IDP information
* from the MongoDB server. The type of the value must be
* {@link OidcCallback}.
* <p>
* If this is provided, {@link MongoCredential#PROVIDER_NAME_KEY}
* and {@link MongoCredential#OIDC_CALLBACK_KEY}
* must not be provided.
*
* @see #createOidcCredential(String)
* @since 4.10
*/
public static final String OIDC_HUMAN_CALLBACK_KEY = "OIDC_HUMAN_CALLBACK";


/**
* Mechanism key for a list of allowed hostnames or ip-addresses for MongoDB connections. Ports must be excluded.
* The hostnames may include a leading "*." wildcard, which allows for matching (potentially nested) subdomains.
* When MONGODB-OIDC authentication is attempted against a hostname that does not match any of list of allowed hosts
* the driver will raise an error. The type of the value must be {@code List<String>}.
*
* @see MongoCredential#DEFAULT_ALLOWED_HOSTS
* @see #createOidcCredential(String)
* @since 4.10
*/
public static final String ALLOWED_HOSTS_KEY = "ALLOWED_HOSTS";

/**
* The list of allowed hosts that will be used if no
* {@link MongoCredential#ALLOWED_HOSTS_KEY} value is supplied.
* The default allowed hosts are:
* {@code "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"}
*
* @see #createOidcCredential(String)
* @since 4.10
*/
public static final List<String> DEFAULT_ALLOWED_HOSTS = Collections.unmodifiableList(Arrays.asList(
"*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1"));

/**
* Creates a MongoCredential instance with an unspecified mechanism. The client will negotiate the best mechanism based on the
* version of the server that the client is authenticating to.
Expand Down Expand Up @@ -365,6 +410,8 @@ public static MongoCredential createAwsCredential(@Nullable final String userNam
* @see #withMechanismProperty(String, Object)
* @see #PROVIDER_NAME_KEY
* @see #OIDC_CALLBACK_KEY
* @see #OIDC_HUMAN_CALLBACK_KEY
* @see #ALLOWED_HOSTS_KEY
* @mongodb.server.release 7.0
*/
public static MongoCredential createOidcCredential(@Nullable final String userName) {
Expand Down Expand Up @@ -593,10 +640,15 @@ public String toString() {
}

/**
* The context for the {@link OidcRequestCallback#onRequest(OidcRequestContext) OIDC request callback}.
* The context for the {@link OidcCallback#onRequest(OidcCallbackContext) OIDC request callback}.
*/
@Evolving
public interface OidcRequestContext {
public interface OidcCallbackContext {
/**
* @return The OIDC Identity Provider's configuration that can be used to acquire an Access Token.
*/
@Nullable
IdpInfo getIdpInfo();

/**
* @return The timeout that this callback must complete within.
Expand All @@ -607,6 +659,12 @@ public interface OidcRequestContext {
* @return The OIDC callback API version. Currently, version 1.
*/
int getVersion();

/**
* @return The OIDC Refresh token supplied by a prior callback invocation.
*/
@Nullable
String getRefreshToken();
}

/**
Expand All @@ -616,27 +674,76 @@ public interface OidcRequestContext {
* It does not have to be thread-safe, unless it is provided to multiple
* MongoClients.
*/
public interface OidcRequestCallback {
public interface OidcCallback {
/**
* @param context The context.
* @return The response produced by an OIDC Identity Provider
*/
RequestCallbackResult onRequest(OidcRequestContext context);
OidcCallbackResult onRequest(OidcCallbackContext context);
}

/**
* The OIDC Identity Provider's configuration that can be used to acquire an Access Token.
*/
@Evolving
public interface IdpInfo {
/**
* @return URL which describes the Authorization Server. This identifier is the
* iss of provided access tokens, and is viable for RFC8414 metadata
* discovery and RFC9207 identification.
*/
String getIssuer();

/**
* @return Unique client ID for this OIDC client.
*/
String getClientId();

/**
* @return Additional scopes to request from Identity Provider. Immutable.
*/
List<String> getRequestScopes();
}

/**
* The response produced by an OIDC Identity Provider.
*/
public static final class RequestCallbackResult {
public static final class OidcCallbackResult {

private final String accessToken;

private final Duration expiresIn;

@Nullable
private final String refreshToken;

/**
* @param accessToken The OIDC access token.
* @param expiresIn Time until the access token expires.
* A {@linkplain Duration#isZero() zero-length} duration
* means that the access token does not expire.
*/
public OidcCallbackResult(final String accessToken, final Duration expiresIn) {
this(accessToken, expiresIn, null);
}

/**
* @param accessToken The OIDC access token
* @param accessToken The OIDC access token.
* @param expiresIn Time until the access token expires.
* A {@linkplain Duration#isZero() zero-length} duration
* means that the access token does not expire.
* @param refreshToken The refresh token. If null, refresh will not be attempted.
*/
public RequestCallbackResult(final String accessToken) {
public OidcCallbackResult(final String accessToken, final Duration expiresIn,
@Nullable final String refreshToken) {
notNull("accessToken", accessToken);
notNull("expiresIn", expiresIn);
if (expiresIn.isNegative()) {
throw new IllegalArgumentException("expiresIn must not be a negative value");
}
this.accessToken = accessToken;
this.expiresIn = expiresIn;
this.refreshToken = refreshToken;
}

/**
Expand All @@ -645,5 +752,13 @@ public RequestCallbackResult(final String accessToken) {
public String getAccessToken() {
return accessToken;
}

/**
* @return The OIDC refresh token. If null, refresh will not be attempted.
*/
@Nullable
public String getRefreshToken() {
return refreshToken;
}
}
}

0 comments on commit 45d1c59

Please sign in to comment.