mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 09:58:14 +00:00
feat(frontend): align frontend sso code with refactors (#9506)
This commit is contained in:
parent
1e64a75339
commit
651998de44
@ -1,13 +1,9 @@
|
||||
package auth;
|
||||
|
||||
import static auth.AuthUtils.*;
|
||||
import static auth.sso.oidc.OidcConfigs.*;
|
||||
import static utils.ConfigUtil.*;
|
||||
|
||||
import auth.sso.SsoConfigs;
|
||||
import auth.sso.SsoManager;
|
||||
import auth.sso.oidc.OidcConfigs;
|
||||
import auth.sso.oidc.OidcProvider;
|
||||
import client.AuthServiceClient;
|
||||
import com.datahub.authentication.Actor;
|
||||
import com.datahub.authentication.ActorType;
|
||||
@ -23,14 +19,11 @@ import com.linkedin.util.Configuration;
|
||||
import config.ConfigurationProvider;
|
||||
import controllers.SsoCallbackController;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.pac4j.core.client.Client;
|
||||
import org.pac4j.core.client.Clients;
|
||||
import org.pac4j.core.config.Config;
|
||||
import org.pac4j.core.context.session.SessionStore;
|
||||
import org.pac4j.play.LogoutController;
|
||||
@ -45,6 +38,7 @@ import play.cache.SyncCacheApi;
|
||||
import utils.ConfigUtil;
|
||||
|
||||
/** Responsible for configuring, validating, and providing authentication related components. */
|
||||
@Slf4j
|
||||
public class AuthModule extends AbstractModule {
|
||||
|
||||
/**
|
||||
@ -58,6 +52,7 @@ public class AuthModule extends AbstractModule {
|
||||
private static final String PAC4J_SESSIONSTORE_PROVIDER_CONF = "pac4j.sessionStore.provider";
|
||||
private static final String ENTITY_CLIENT_RETRY_INTERVAL = "entityClient.retryInterval";
|
||||
private static final String ENTITY_CLIENT_NUM_RETRIES = "entityClient.numRetries";
|
||||
private static final String GET_SSO_SETTINGS_ENDPOINT = "auth/getSsoSettings";
|
||||
|
||||
private final com.typesafe.config.Config _configs;
|
||||
|
||||
@ -111,6 +106,7 @@ public class AuthModule extends AbstractModule {
|
||||
Authentication.class,
|
||||
SystemEntityClient.class,
|
||||
AuthServiceClient.class,
|
||||
org.pac4j.core.config.Config.class,
|
||||
com.typesafe.config.Config.class));
|
||||
} catch (NoSuchMethodException | SecurityException e) {
|
||||
throw new RuntimeException(
|
||||
@ -124,34 +120,20 @@ public class AuthModule extends AbstractModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected Config provideConfig(SsoManager ssoManager) {
|
||||
if (ssoManager.isSsoEnabled()) {
|
||||
final Clients clients = new Clients();
|
||||
final List<Client> clientList = new ArrayList<>();
|
||||
clientList.add(ssoManager.getSsoProvider().client());
|
||||
clients.setClients(clientList);
|
||||
final Config config = new Config(clients);
|
||||
config.setHttpActionAdapter(new PlayHttpActionAdapter());
|
||||
return config;
|
||||
}
|
||||
return new Config();
|
||||
protected Config provideConfig() {
|
||||
Config config = new Config();
|
||||
config.setHttpActionAdapter(new PlayHttpActionAdapter());
|
||||
return config;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected SsoManager provideSsoManager() {
|
||||
SsoManager manager = new SsoManager();
|
||||
// Seed the SSO manager with a default SSO provider.
|
||||
if (isSsoEnabled(_configs)) {
|
||||
SsoConfigs ssoConfigs = new SsoConfigs(_configs);
|
||||
if (ssoConfigs.isOidcEnabled()) {
|
||||
// Register OIDC Provider, add to list of managers.
|
||||
OidcConfigs oidcConfigs = new OidcConfigs(_configs);
|
||||
OidcProvider oidcProvider = new OidcProvider(oidcConfigs);
|
||||
// Set the default SSO provider to this OIDC client.
|
||||
manager.setSsoProvider(oidcProvider);
|
||||
}
|
||||
}
|
||||
protected SsoManager provideSsoManager(
|
||||
Authentication systemAuthentication, CloseableHttpClient httpClient) {
|
||||
SsoManager manager =
|
||||
new SsoManager(
|
||||
_configs, systemAuthentication, getSsoSettingsRequestUrl(_configs), httpClient);
|
||||
manager.initializeSsoProvider();
|
||||
return manager;
|
||||
}
|
||||
|
||||
@ -191,33 +173,16 @@ public class AuthModule extends AbstractModule {
|
||||
configurationProvider.getCache().getClient().getEntityClient());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected CloseableHttpClient provideHttpClient() {
|
||||
return HttpClients.createDefault();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected AuthServiceClient provideAuthClient(
|
||||
Authentication systemAuthentication, CloseableHttpClient httpClient) {
|
||||
// Init a GMS auth client
|
||||
final String metadataServiceHost =
|
||||
_configs.hasPath(METADATA_SERVICE_HOST_CONFIG_PATH)
|
||||
? _configs.getString(METADATA_SERVICE_HOST_CONFIG_PATH)
|
||||
: Configuration.getEnvironmentVariable(GMS_HOST_ENV_VAR, DEFAULT_GMS_HOST);
|
||||
final String metadataServiceHost = getMetadataServiceHost(_configs);
|
||||
|
||||
final int metadataServicePort =
|
||||
_configs.hasPath(METADATA_SERVICE_PORT_CONFIG_PATH)
|
||||
? _configs.getInt(METADATA_SERVICE_PORT_CONFIG_PATH)
|
||||
: Integer.parseInt(
|
||||
Configuration.getEnvironmentVariable(GMS_PORT_ENV_VAR, DEFAULT_GMS_PORT));
|
||||
final int metadataServicePort = getMetadataServicePort(_configs);
|
||||
|
||||
final Boolean metadataServiceUseSsl =
|
||||
_configs.hasPath(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
||||
? _configs.getBoolean(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
||||
: Boolean.parseBoolean(
|
||||
Configuration.getEnvironmentVariable(GMS_USE_SSL_ENV_VAR, DEFAULT_GMS_USE_SSL));
|
||||
final boolean metadataServiceUseSsl = doesMetadataServiceUseSsl(_configs);
|
||||
|
||||
return new AuthServiceClient(
|
||||
metadataServiceHost,
|
||||
@ -227,6 +192,12 @@ public class AuthModule extends AbstractModule {
|
||||
httpClient);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
protected CloseableHttpClient provideHttpClient() {
|
||||
return HttpClients.createDefault();
|
||||
}
|
||||
|
||||
private com.linkedin.restli.client.Client buildRestliClient() {
|
||||
final String metadataServiceHost =
|
||||
utils.ConfigUtil.getString(
|
||||
@ -255,16 +226,33 @@ public class AuthModule extends AbstractModule {
|
||||
metadataServiceSslProtocol);
|
||||
}
|
||||
|
||||
protected boolean isSsoEnabled(com.typesafe.config.Config configs) {
|
||||
// If OIDC is enabled, we infer SSO to be enabled.
|
||||
return configs.hasPath(OIDC_ENABLED_CONFIG_PATH)
|
||||
&& Boolean.TRUE.equals(Boolean.parseBoolean(configs.getString(OIDC_ENABLED_CONFIG_PATH)));
|
||||
protected boolean doesMetadataServiceUseSsl(com.typesafe.config.Config configs) {
|
||||
return configs.hasPath(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
||||
? configs.getBoolean(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
||||
: Boolean.parseBoolean(
|
||||
Configuration.getEnvironmentVariable(GMS_USE_SSL_ENV_VAR, DEFAULT_GMS_USE_SSL));
|
||||
}
|
||||
|
||||
protected boolean isMetadataServiceAuthEnabled(com.typesafe.config.Config configs) {
|
||||
// If OIDC is enabled, we infer SSO to be enabled.
|
||||
return configs.hasPath(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH)
|
||||
&& Boolean.TRUE.equals(
|
||||
Boolean.parseBoolean(configs.getString(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH)));
|
||||
protected String getMetadataServiceHost(com.typesafe.config.Config configs) {
|
||||
return configs.hasPath(METADATA_SERVICE_HOST_CONFIG_PATH)
|
||||
? configs.getString(METADATA_SERVICE_HOST_CONFIG_PATH)
|
||||
: Configuration.getEnvironmentVariable(GMS_HOST_ENV_VAR, DEFAULT_GMS_HOST);
|
||||
}
|
||||
|
||||
protected Integer getMetadataServicePort(com.typesafe.config.Config configs) {
|
||||
return configs.hasPath(METADATA_SERVICE_PORT_CONFIG_PATH)
|
||||
? configs.getInt(METADATA_SERVICE_PORT_CONFIG_PATH)
|
||||
: Integer.parseInt(
|
||||
Configuration.getEnvironmentVariable(GMS_PORT_ENV_VAR, DEFAULT_GMS_PORT));
|
||||
}
|
||||
|
||||
protected String getSsoSettingsRequestUrl(com.typesafe.config.Config configs) {
|
||||
final String protocol = doesMetadataServiceUseSsl(configs) ? "https" : "http";
|
||||
final String metadataServiceHost = getMetadataServiceHost(configs);
|
||||
final Integer metadataServicePort = getMetadataServicePort(configs);
|
||||
|
||||
return String.format(
|
||||
"%s://%s:%s/%s",
|
||||
protocol, metadataServiceHost, metadataServicePort, GET_SSO_SETTINGS_ENDPOINT);
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,26 @@ public class AuthUtils {
|
||||
public static final String TITLE = "title";
|
||||
public static final String INVITE_TOKEN = "inviteToken";
|
||||
public static final String RESET_TOKEN = "resetToken";
|
||||
public static final String BASE_URL = "baseUrl";
|
||||
public static final String OIDC_ENABLED = "oidcEnabled";
|
||||
public static final String CLIENT_ID = "clientId";
|
||||
public static final String CLIENT_SECRET = "clientSecret";
|
||||
public static final String DISCOVERY_URI = "discoveryUri";
|
||||
|
||||
public static final String USER_NAME_CLAIM = "userNameClaim";
|
||||
public static final String USER_NAME_CLAIM_REGEX = "userNameClaimRegex";
|
||||
public static final String SCOPE = "scope";
|
||||
public static final String CLIENT_NAME = "clientName";
|
||||
public static final String CLIENT_AUTHENTICATION_METHOD = "clientAuthenticationMethod";
|
||||
public static final String JIT_PROVISIONING_ENABLED = "jitProvisioningEnabled";
|
||||
public static final String PRE_PROVISIONING_REQUIRED = "preProvisioningRequired";
|
||||
public static final String EXTRACT_GROUPS_ENABLED = "extractGroupsEnabled";
|
||||
public static final String GROUPS_CLAIM = "groupsClaim";
|
||||
public static final String RESPONSE_TYPE = "responseType";
|
||||
public static final String RESPONSE_MODE = "responseMode";
|
||||
public static final String USE_NONCE = "useNonce";
|
||||
public static final String READ_TIMEOUT = "readTimeout";
|
||||
public static final String EXTRACT_JWT_ACCESS_TOKEN_CLAIMS = "extractJwtAccessTokenClaims";
|
||||
|
||||
/**
|
||||
* Determines whether the inbound request should be forward to downstream Metadata Service. Today,
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
package auth.sso;
|
||||
|
||||
import static auth.ConfigUtil.*;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/** Class responsible for extracting and validating top-level SSO related configurations. */
|
||||
import static auth.AuthUtils.*;
|
||||
|
||||
|
||||
/**
|
||||
* Class responsible for extracting and validating top-level SSO related configurations. TODO:
|
||||
* Refactor SsoConfigs to have OidcConfigs and other identity provider specific configs as instance
|
||||
* variables. SSoManager should ideally not know about identity provider specific configs.
|
||||
*/
|
||||
public class SsoConfigs {
|
||||
|
||||
/** Required configs */
|
||||
@ -22,16 +30,11 @@ public class SsoConfigs {
|
||||
private final String _authSuccessRedirectPath;
|
||||
private final Boolean _oidcEnabled;
|
||||
|
||||
public SsoConfigs(final com.typesafe.config.Config configs) {
|
||||
_authBaseUrl = getRequired(configs, AUTH_BASE_URL_CONFIG_PATH);
|
||||
_authBaseCallbackPath =
|
||||
getOptional(configs, AUTH_BASE_CALLBACK_PATH_CONFIG_PATH, DEFAULT_BASE_CALLBACK_PATH);
|
||||
_authSuccessRedirectPath =
|
||||
getOptional(configs, AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH, DEFAULT_SUCCESS_REDIRECT_PATH);
|
||||
_oidcEnabled =
|
||||
configs.hasPath(OIDC_ENABLED_CONFIG_PATH)
|
||||
&& Boolean.TRUE.equals(
|
||||
Boolean.parseBoolean(configs.getString(OIDC_ENABLED_CONFIG_PATH)));
|
||||
public SsoConfigs(Builder<?> builder) {
|
||||
_authBaseUrl = builder._authBaseUrl;
|
||||
_authBaseCallbackPath = builder._authBaseCallbackPath;
|
||||
_authSuccessRedirectPath = builder._authSuccessRedirectPath;
|
||||
_oidcEnabled = builder._oidcEnabled;
|
||||
}
|
||||
|
||||
public String getAuthBaseUrl() {
|
||||
@ -49,4 +52,52 @@ public class SsoConfigs {
|
||||
public Boolean isOidcEnabled() {
|
||||
return _oidcEnabled;
|
||||
}
|
||||
|
||||
public static class Builder<T extends Builder<T>> {
|
||||
protected String _authBaseUrl = null;
|
||||
private String _authBaseCallbackPath = DEFAULT_BASE_CALLBACK_PATH;
|
||||
private String _authSuccessRedirectPath = DEFAULT_SUCCESS_REDIRECT_PATH;
|
||||
protected Boolean _oidcEnabled = false;
|
||||
private final ObjectMapper _objectMapper = new ObjectMapper();
|
||||
protected JsonNode jsonNode = null;
|
||||
|
||||
// No need to check if changes are made since this method is only called at start-up.
|
||||
public Builder from(final com.typesafe.config.Config configs) {
|
||||
if (configs.hasPath(AUTH_BASE_URL_CONFIG_PATH)) {
|
||||
_authBaseUrl = configs.getString(AUTH_BASE_URL_CONFIG_PATH);
|
||||
}
|
||||
if (configs.hasPath(AUTH_BASE_CALLBACK_PATH_CONFIG_PATH)) {
|
||||
_authBaseCallbackPath = configs.getString(AUTH_BASE_CALLBACK_PATH_CONFIG_PATH);
|
||||
}
|
||||
if (configs.hasPath(OIDC_ENABLED_CONFIG_PATH)) {
|
||||
_oidcEnabled =
|
||||
Boolean.TRUE.equals(Boolean.parseBoolean(configs.getString(OIDC_ENABLED_CONFIG_PATH)));
|
||||
}
|
||||
if (configs.hasPath(AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH)) {
|
||||
_authSuccessRedirectPath = configs.getString(AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from(String ssoSettingsJsonStr) {
|
||||
try {
|
||||
jsonNode = _objectMapper.readTree(ssoSettingsJsonStr);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
String.format("Failed to parse ssoSettingsJsonStr %s into JSON", ssoSettingsJsonStr));
|
||||
}
|
||||
if (jsonNode.has(BASE_URL)) {
|
||||
_authBaseUrl = jsonNode.get(BASE_URL).asText();
|
||||
}
|
||||
if (jsonNode.has(OIDC_ENABLED)) {
|
||||
_oidcEnabled = jsonNode.get(OIDC_ENABLED).asBoolean();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SsoConfigs build() {
|
||||
return new SsoConfigs(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,49 @@
|
||||
package auth.sso;
|
||||
|
||||
import auth.sso.oidc.OidcConfigs;
|
||||
import auth.sso.oidc.OidcProvider;
|
||||
import com.datahub.authentication.Authentication;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import play.mvc.Http;
|
||||
|
||||
/** Singleton class that stores & serves reference to a single {@link SsoProvider} if one exists. */
|
||||
/**
|
||||
* Singleton class that stores & serves reference to a single {@link SsoProvider} if one exists.
|
||||
* TODO: Refactor SsoManager to only accept SsoConfigs when initialized. See SsoConfigs TODO as
|
||||
* well.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SsoManager {
|
||||
|
||||
private SsoProvider<?> _provider; // Only one active provider at a time.
|
||||
private final Authentication
|
||||
_authentication; // Authentication used to fetch SSO settings from GMS
|
||||
private final String _ssoSettingsRequestUrl; // SSO settings request URL.
|
||||
private final CloseableHttpClient _httpClient; // HTTP client for making requests to GMS.
|
||||
private com.typesafe.config.Config _configs;
|
||||
|
||||
public SsoManager() {}
|
||||
public SsoManager(
|
||||
com.typesafe.config.Config configs,
|
||||
Authentication authentication,
|
||||
String ssoSettingsRequestUrl,
|
||||
CloseableHttpClient httpClient) {
|
||||
_configs = configs;
|
||||
_authentication = Objects.requireNonNull(authentication, "authentication cannot be null");
|
||||
_ssoSettingsRequestUrl =
|
||||
Objects.requireNonNull(ssoSettingsRequestUrl, "ssoSettingsRequestUrl cannot be null");
|
||||
_httpClient = Objects.requireNonNull(httpClient, "httpClient cannot be null");
|
||||
_provider = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if SSO is enabled, meaning a non-null {@link SsoProvider} has been provided to the
|
||||
@ -16,6 +52,7 @@ public class SsoManager {
|
||||
* @return true if SSO logic is enabled, false otherwise.
|
||||
*/
|
||||
public boolean isSsoEnabled() {
|
||||
refreshSsoProvider();
|
||||
return _provider != null;
|
||||
}
|
||||
|
||||
@ -24,17 +61,138 @@ public class SsoManager {
|
||||
*
|
||||
* @param provider the new {@link SsoProvider} to be used during authentication.
|
||||
*/
|
||||
public void setSsoProvider(@Nonnull final SsoProvider<?> provider) {
|
||||
public void setSsoProvider(final SsoProvider<?> provider) {
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
public void setConfigs(final com.typesafe.config.Config configs) {
|
||||
_configs = configs;
|
||||
}
|
||||
|
||||
public void clearSsoProvider() {
|
||||
_provider = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active {@link SsoProvider} instance.
|
||||
*
|
||||
* @return the {@SsoProvider} that should be used during authentication and on IdP callback, or
|
||||
* null if SSO is not enabled.
|
||||
*/
|
||||
@Nullable
|
||||
public SsoProvider<?> getSsoProvider() {
|
||||
return _provider;
|
||||
}
|
||||
|
||||
public void initializeSsoProvider() {
|
||||
SsoConfigs ssoConfigs = null;
|
||||
try {
|
||||
ssoConfigs = new SsoConfigs.Builder().from(_configs).build();
|
||||
} catch (Exception e) {
|
||||
// Debug-level logging since this is expected to fail if SSO has not been configured.
|
||||
log.debug(String.format("Missing SSO settings in static configs %s", _configs), e);
|
||||
}
|
||||
|
||||
if (ssoConfigs != null && ssoConfigs.isOidcEnabled()) {
|
||||
try {
|
||||
OidcConfigs oidcConfigs = new OidcConfigs.Builder().from(_configs).build();
|
||||
maybeUpdateOidcProvider(oidcConfigs);
|
||||
} catch (Exception e) {
|
||||
// Error-level logging since this is unexpected to fail if SSO has been configured.
|
||||
log.error(String.format("Error building OidcConfigs from static configs %s", _configs), e);
|
||||
}
|
||||
} else {
|
||||
// Clear the SSO Provider since no SSO is enabled.
|
||||
clearSsoProvider();
|
||||
}
|
||||
|
||||
refreshSsoProvider();
|
||||
}
|
||||
|
||||
private void refreshSsoProvider() {
|
||||
final Optional<String> maybeSsoSettingsJsonStr = getDynamicSsoSettings();
|
||||
if (maybeSsoSettingsJsonStr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we receive a non-empty response, try to update the SSO provider.
|
||||
final String ssoSettingsJsonStr = maybeSsoSettingsJsonStr.get();
|
||||
SsoConfigs ssoConfigs;
|
||||
try {
|
||||
ssoConfigs = new SsoConfigs.Builder().from(ssoSettingsJsonStr).build();
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Error building SsoConfigs from invalid json %s, reusing previous settings",
|
||||
ssoSettingsJsonStr),
|
||||
e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ssoConfigs != null && ssoConfigs.isOidcEnabled()) {
|
||||
try {
|
||||
OidcConfigs oidcConfigs =
|
||||
new OidcConfigs.Builder().from(_configs, ssoSettingsJsonStr).build();
|
||||
maybeUpdateOidcProvider(oidcConfigs);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Error building OidcConfigs from invalid json %s, reusing previous settings",
|
||||
ssoSettingsJsonStr),
|
||||
e);
|
||||
}
|
||||
} else {
|
||||
// Clear the SSO Provider since no SSO is enabled.
|
||||
clearSsoProvider();
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateOidcProvider(OidcConfigs oidcConfigs) {
|
||||
SsoProvider existingSsoProvider = getSsoProvider();
|
||||
if (existingSsoProvider instanceof OidcProvider) {
|
||||
OidcProvider existingOidcProvider = (OidcProvider) existingSsoProvider;
|
||||
// If the existing provider is an OIDC provider and the configs are the same, do nothing.
|
||||
if (existingOidcProvider.configs().equals(oidcConfigs)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OidcProvider oidcProvider = new OidcProvider(oidcConfigs);
|
||||
setSsoProvider(oidcProvider);
|
||||
}
|
||||
|
||||
/** Call the Auth Service to get SSO settings */
|
||||
@Nonnull
|
||||
private Optional<String> getDynamicSsoSettings() {
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
final HttpPost request = new HttpPost(_ssoSettingsRequestUrl);
|
||||
|
||||
// Build JSON request to verify credentials for a native user.
|
||||
request.setEntity(new StringEntity(""));
|
||||
|
||||
// Add authorization header with DataHub frontend system id and secret.
|
||||
request.addHeader(Http.HeaderNames.AUTHORIZATION, _authentication.getCredentials());
|
||||
|
||||
response = _httpClient.execute(request);
|
||||
final HttpEntity entity = response.getEntity();
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK && entity != null) {
|
||||
// Successfully received the SSO settings
|
||||
return Optional.of(EntityUtils.toString(entity));
|
||||
} else {
|
||||
log.debug("No SSO settings received from Auth Service, reusing previous settings");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to get SSO settings due to exception, reusing previous settings", e);
|
||||
} finally {
|
||||
try {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to close http response", e);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package auth.sso.oidc;
|
||||
|
||||
import static auth.AuthUtils.*;
|
||||
import static auth.ConfigUtil.*;
|
||||
|
||||
import auth.sso.SsoConfigs;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
|
||||
@ -44,79 +46,204 @@ public class OidcConfigs extends SsoConfigs {
|
||||
private static final String DEFAULT_OIDC_USERNAME_CLAIM = "email";
|
||||
|
||||
private static final String DEFAULT_OIDC_USERNAME_CLAIM_REGEX = "(.*)";
|
||||
private static final String DEFAULT_OIDC_SCOPE =
|
||||
"openid profile email"; // Often "group" must be included for groups.
|
||||
private static final String DEFAULT_OIDC_SCOPE = "openid profile email";
|
||||
// Often "group" must be included for groups.
|
||||
private static final String DEFAULT_OIDC_CLIENT_NAME = "oidc";
|
||||
private static final String DEFAULT_OIDC_CLIENT_AUTHENTICATION_METHOD = "client_secret_basic";
|
||||
private static final String DEFAULT_OIDC_JIT_PROVISIONING_ENABLED = "true";
|
||||
private static final String DEFAULT_OIDC_PRE_PROVISIONING_REQUIRED = "false";
|
||||
private static final String DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED =
|
||||
"false"; // False since extraction of groups can overwrite existing group membership.
|
||||
private static final String DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED = "false";
|
||||
// False since extraction of groups can overwrite existing group membership.
|
||||
private static final String DEFAULT_OIDC_GROUPS_CLAIM = "groups";
|
||||
private static final String DEFAULT_OIDC_READ_TIMEOUT = "5000";
|
||||
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private String discoveryUri;
|
||||
private String userNameClaim;
|
||||
private String userNameClaimRegex;
|
||||
private String scope;
|
||||
private String clientName;
|
||||
private String clientAuthenticationMethod;
|
||||
private boolean jitProvisioningEnabled;
|
||||
private boolean preProvisioningRequired;
|
||||
private boolean extractGroupsEnabled;
|
||||
private String groupsClaimName;
|
||||
private Optional<String> responseType;
|
||||
private Optional<String> responseMode;
|
||||
private Optional<Boolean> useNonce;
|
||||
private Optional<String> customParamResource;
|
||||
private String readTimeout;
|
||||
private Optional<Boolean> extractJwtAccessTokenClaims;
|
||||
private final String clientId;
|
||||
private final String clientSecret;
|
||||
private final String discoveryUri;
|
||||
private final String userNameClaim;
|
||||
private final String userNameClaimRegex;
|
||||
private final String scope;
|
||||
private final String clientName;
|
||||
private final String clientAuthenticationMethod;
|
||||
private final boolean jitProvisioningEnabled;
|
||||
private final boolean preProvisioningRequired;
|
||||
private final boolean extractGroupsEnabled;
|
||||
private final String groupsClaimName;
|
||||
private final Optional<String> responseType;
|
||||
private final Optional<String> responseMode;
|
||||
private final Optional<Boolean> useNonce;
|
||||
private final Optional<String> customParamResource;
|
||||
private final String readTimeout;
|
||||
private final Optional<Boolean> extractJwtAccessTokenClaims;
|
||||
private Optional<String> preferredJwsAlgorithm;
|
||||
|
||||
public OidcConfigs(final com.typesafe.config.Config configs) {
|
||||
super(configs);
|
||||
clientId = getRequired(configs, OIDC_CLIENT_ID_CONFIG_PATH);
|
||||
clientSecret = getRequired(configs, OIDC_CLIENT_SECRET_CONFIG_PATH);
|
||||
discoveryUri = getRequired(configs, OIDC_DISCOVERY_URI_CONFIG_PATH);
|
||||
userNameClaim =
|
||||
getOptional(configs, OIDC_USERNAME_CLAIM_CONFIG_PATH, DEFAULT_OIDC_USERNAME_CLAIM);
|
||||
userNameClaimRegex =
|
||||
getOptional(
|
||||
configs, OIDC_USERNAME_CLAIM_REGEX_CONFIG_PATH, DEFAULT_OIDC_USERNAME_CLAIM_REGEX);
|
||||
scope = getOptional(configs, OIDC_SCOPE_CONFIG_PATH, DEFAULT_OIDC_SCOPE);
|
||||
clientName = getOptional(configs, OIDC_CLIENT_NAME_CONFIG_PATH, DEFAULT_OIDC_CLIENT_NAME);
|
||||
clientAuthenticationMethod =
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_CLIENT_AUTHENTICATION_METHOD_CONFIG_PATH,
|
||||
DEFAULT_OIDC_CLIENT_AUTHENTICATION_METHOD);
|
||||
jitProvisioningEnabled =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_JIT_PROVISIONING_ENABLED_CONFIG_PATH,
|
||||
DEFAULT_OIDC_JIT_PROVISIONING_ENABLED));
|
||||
preProvisioningRequired =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_PRE_PROVISIONING_REQUIRED_CONFIG_PATH,
|
||||
DEFAULT_OIDC_PRE_PROVISIONING_REQUIRED));
|
||||
extractGroupsEnabled =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(configs, OIDC_EXTRACT_GROUPS_ENABLED, DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED));
|
||||
groupsClaimName =
|
||||
getOptional(configs, OIDC_GROUPS_CLAIM_CONFIG_PATH_CONFIG_PATH, DEFAULT_OIDC_GROUPS_CLAIM);
|
||||
responseType = getOptional(configs, OIDC_RESPONSE_TYPE);
|
||||
responseMode = getOptional(configs, OIDC_RESPONSE_MODE);
|
||||
useNonce = getOptional(configs, OIDC_USE_NONCE).map(Boolean::parseBoolean);
|
||||
customParamResource = getOptional(configs, OIDC_CUSTOM_PARAM_RESOURCE);
|
||||
readTimeout = getOptional(configs, OIDC_READ_TIMEOUT, DEFAULT_OIDC_READ_TIMEOUT);
|
||||
extractJwtAccessTokenClaims =
|
||||
getOptional(configs, OIDC_EXTRACT_JWT_ACCESS_TOKEN_CLAIMS).map(Boolean::parseBoolean);
|
||||
preferredJwsAlgorithm =
|
||||
Optional.ofNullable(getOptional(configs, OIDC_PREFERRED_JWS_ALGORITHM, null));
|
||||
public OidcConfigs(Builder builder) {
|
||||
super(builder);
|
||||
this.clientId = builder.clientId;
|
||||
this.clientSecret = builder.clientSecret;
|
||||
this.discoveryUri = builder.discoveryUri;
|
||||
this.userNameClaim = builder.userNameClaim;
|
||||
this.userNameClaimRegex = builder.userNameClaimRegex;
|
||||
this.scope = builder.scope;
|
||||
this.clientName = builder.clientName;
|
||||
this.clientAuthenticationMethod = builder.clientAuthenticationMethod;
|
||||
this.jitProvisioningEnabled = builder.jitProvisioningEnabled;
|
||||
this.preProvisioningRequired = builder.preProvisioningRequired;
|
||||
this.extractGroupsEnabled = builder.extractGroupsEnabled;
|
||||
this.groupsClaimName = builder.groupsClaimName;
|
||||
this.responseType = builder.responseType;
|
||||
this.responseMode = builder.responseMode;
|
||||
this.useNonce = builder.useNonce;
|
||||
this.customParamResource = builder.customParamResource;
|
||||
this.readTimeout = builder.readTimeout;
|
||||
this.extractJwtAccessTokenClaims = builder.extractJwtAccessTokenClaims;
|
||||
this.preferredJwsAlgorithm = builder.preferredJwsAlgorithm;
|
||||
}
|
||||
|
||||
public static class Builder extends SsoConfigs.Builder<Builder> {
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private String discoveryUri;
|
||||
private String userNameClaim = DEFAULT_OIDC_USERNAME_CLAIM;
|
||||
private String userNameClaimRegex = DEFAULT_OIDC_USERNAME_CLAIM_REGEX;
|
||||
private String scope = DEFAULT_OIDC_SCOPE;
|
||||
private String clientName = DEFAULT_OIDC_CLIENT_NAME;
|
||||
private String clientAuthenticationMethod = DEFAULT_OIDC_CLIENT_AUTHENTICATION_METHOD;
|
||||
private boolean jitProvisioningEnabled =
|
||||
Boolean.parseBoolean(DEFAULT_OIDC_JIT_PROVISIONING_ENABLED);
|
||||
private boolean preProvisioningRequired =
|
||||
Boolean.parseBoolean(DEFAULT_OIDC_PRE_PROVISIONING_REQUIRED);
|
||||
private boolean extractGroupsEnabled =
|
||||
Boolean.parseBoolean(DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED);
|
||||
private String groupsClaimName = DEFAULT_OIDC_GROUPS_CLAIM;
|
||||
private Optional<String> responseType = Optional.empty();
|
||||
private Optional<String> responseMode = Optional.empty();
|
||||
private Optional<Boolean> useNonce = Optional.empty();
|
||||
private Optional<String> customParamResource = Optional.empty();
|
||||
private String readTimeout = DEFAULT_OIDC_READ_TIMEOUT;
|
||||
private Optional<Boolean> extractJwtAccessTokenClaims = Optional.empty();
|
||||
private Optional<String> preferredJwsAlgorithm = Optional.empty();
|
||||
|
||||
public Builder from(final com.typesafe.config.Config configs) {
|
||||
super.from(configs);
|
||||
clientId = getRequired(configs, OIDC_CLIENT_ID_CONFIG_PATH);
|
||||
clientSecret = getRequired(configs, OIDC_CLIENT_SECRET_CONFIG_PATH);
|
||||
discoveryUri = getRequired(configs, OIDC_DISCOVERY_URI_CONFIG_PATH);
|
||||
userNameClaim =
|
||||
getOptional(configs, OIDC_USERNAME_CLAIM_CONFIG_PATH, DEFAULT_OIDC_USERNAME_CLAIM);
|
||||
userNameClaimRegex =
|
||||
getOptional(
|
||||
configs, OIDC_USERNAME_CLAIM_REGEX_CONFIG_PATH, DEFAULT_OIDC_USERNAME_CLAIM_REGEX);
|
||||
scope = getOptional(configs, OIDC_SCOPE_CONFIG_PATH, DEFAULT_OIDC_SCOPE);
|
||||
clientName = getOptional(configs, OIDC_CLIENT_NAME_CONFIG_PATH, DEFAULT_OIDC_CLIENT_NAME);
|
||||
clientAuthenticationMethod =
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_CLIENT_AUTHENTICATION_METHOD_CONFIG_PATH,
|
||||
DEFAULT_OIDC_CLIENT_AUTHENTICATION_METHOD);
|
||||
jitProvisioningEnabled =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_JIT_PROVISIONING_ENABLED_CONFIG_PATH,
|
||||
DEFAULT_OIDC_JIT_PROVISIONING_ENABLED));
|
||||
preProvisioningRequired =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(
|
||||
configs,
|
||||
OIDC_PRE_PROVISIONING_REQUIRED_CONFIG_PATH,
|
||||
DEFAULT_OIDC_PRE_PROVISIONING_REQUIRED));
|
||||
extractGroupsEnabled =
|
||||
Boolean.parseBoolean(
|
||||
getOptional(
|
||||
configs, OIDC_EXTRACT_GROUPS_ENABLED, DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED));
|
||||
groupsClaimName =
|
||||
getOptional(
|
||||
configs, OIDC_GROUPS_CLAIM_CONFIG_PATH_CONFIG_PATH, DEFAULT_OIDC_GROUPS_CLAIM);
|
||||
responseType = getOptional(configs, OIDC_RESPONSE_TYPE);
|
||||
responseMode = getOptional(configs, OIDC_RESPONSE_MODE);
|
||||
useNonce = getOptional(configs, OIDC_USE_NONCE).map(Boolean::parseBoolean);
|
||||
customParamResource = getOptional(configs, OIDC_CUSTOM_PARAM_RESOURCE);
|
||||
readTimeout = getOptional(configs, OIDC_READ_TIMEOUT, DEFAULT_OIDC_READ_TIMEOUT);
|
||||
extractJwtAccessTokenClaims =
|
||||
getOptional(configs, OIDC_EXTRACT_JWT_ACCESS_TOKEN_CLAIMS).map(Boolean::parseBoolean);
|
||||
preferredJwsAlgorithm =
|
||||
Optional.ofNullable(getOptional(configs, OIDC_PREFERRED_JWS_ALGORITHM, null));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from(final com.typesafe.config.Config configs, final String ssoSettingsJsonStr) {
|
||||
super.from(ssoSettingsJsonStr);
|
||||
if (jsonNode.has(CLIENT_ID)) {
|
||||
clientId = jsonNode.get(CLIENT_ID).asText();
|
||||
}
|
||||
if (jsonNode.has(CLIENT_SECRET)) {
|
||||
clientSecret = jsonNode.get(CLIENT_SECRET).asText();
|
||||
}
|
||||
if (jsonNode.has(DISCOVERY_URI)) {
|
||||
discoveryUri = jsonNode.get(DISCOVERY_URI).asText();
|
||||
}
|
||||
if (jsonNode.has(USER_NAME_CLAIM)) {
|
||||
userNameClaim = jsonNode.get(USER_NAME_CLAIM).asText();
|
||||
}
|
||||
if (jsonNode.has(USER_NAME_CLAIM_REGEX)) {
|
||||
userNameClaimRegex = jsonNode.get(USER_NAME_CLAIM_REGEX).asText();
|
||||
}
|
||||
if (jsonNode.has(SCOPE)) {
|
||||
scope = jsonNode.get(SCOPE).asText();
|
||||
}
|
||||
if (jsonNode.has(CLIENT_NAME)) {
|
||||
clientName = jsonNode.get(CLIENT_NAME).asText();
|
||||
}
|
||||
if (jsonNode.has(CLIENT_AUTHENTICATION_METHOD)) {
|
||||
clientAuthenticationMethod = jsonNode.get(CLIENT_AUTHENTICATION_METHOD).asText();
|
||||
}
|
||||
if (jsonNode.has(JIT_PROVISIONING_ENABLED)) {
|
||||
jitProvisioningEnabled = jsonNode.get(JIT_PROVISIONING_ENABLED).asBoolean();
|
||||
}
|
||||
if (jsonNode.has(PRE_PROVISIONING_REQUIRED)) {
|
||||
preProvisioningRequired = jsonNode.get(PRE_PROVISIONING_REQUIRED).asBoolean();
|
||||
}
|
||||
if (jsonNode.has(EXTRACT_GROUPS_ENABLED)) {
|
||||
extractGroupsEnabled = jsonNode.get(EXTRACT_GROUPS_ENABLED).asBoolean();
|
||||
}
|
||||
if (jsonNode.has(GROUPS_CLAIM)) {
|
||||
groupsClaimName = jsonNode.get(GROUPS_CLAIM).asText();
|
||||
}
|
||||
if (jsonNode.has(RESPONSE_TYPE)) {
|
||||
responseType = Optional.of(jsonNode.get(RESPONSE_TYPE).asText());
|
||||
}
|
||||
if (jsonNode.has(RESPONSE_MODE)) {
|
||||
responseMode = Optional.of(jsonNode.get(RESPONSE_MODE).asText());
|
||||
}
|
||||
if (jsonNode.has(USE_NONCE)) {
|
||||
useNonce = Optional.of(jsonNode.get(USE_NONCE).asBoolean());
|
||||
}
|
||||
if (jsonNode.has(READ_TIMEOUT)) {
|
||||
readTimeout = jsonNode.get(READ_TIMEOUT).asText();
|
||||
}
|
||||
if (jsonNode.has(EXTRACT_JWT_ACCESS_TOKEN_CLAIMS)) {
|
||||
extractJwtAccessTokenClaims =
|
||||
Optional.of(jsonNode.get(EXTRACT_JWT_ACCESS_TOKEN_CLAIMS).asBoolean());
|
||||
}
|
||||
if (jsonNode.has(OIDC_PREFERRED_JWS_ALGORITHM)) {
|
||||
preferredJwsAlgorithm = Optional.of(jsonNode.get(OIDC_PREFERRED_JWS_ALGORITHM).asText());
|
||||
} else {
|
||||
preferredJwsAlgorithm =
|
||||
Optional.ofNullable(getOptional(configs, OIDC_PREFERRED_JWS_ALGORITHM, null));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public OidcConfigs build() {
|
||||
Objects.requireNonNull(_oidcEnabled, "oidcEnabled is required");
|
||||
Objects.requireNonNull(clientId, "clientId is required");
|
||||
Objects.requireNonNull(clientSecret, "clientSecret is required");
|
||||
Objects.requireNonNull(discoveryUri, "discoveryUri is required");
|
||||
Objects.requireNonNull(_authBaseUrl, "authBaseUrl is required");
|
||||
|
||||
return new OidcConfigs(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package client;
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.inject.Inject;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nonnull;
|
||||
@ -47,6 +48,7 @@ public class AuthServiceClient {
|
||||
private final Authentication systemAuthentication;
|
||||
private final CloseableHttpClient httpClient;
|
||||
|
||||
@Inject
|
||||
public AuthServiceClient(
|
||||
@Nonnull final String metadataServiceHost,
|
||||
@Nonnull final Integer metadataServicePort,
|
||||
|
||||
@ -9,11 +9,15 @@ import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.entity.client.SystemEntityClient;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pac4j.core.client.Client;
|
||||
import org.pac4j.core.client.Clients;
|
||||
import org.pac4j.core.config.Config;
|
||||
import org.pac4j.core.engine.CallbackLogic;
|
||||
import org.pac4j.core.http.adapter.HttpActionAdapter;
|
||||
@ -34,6 +38,7 @@ import play.mvc.Results;
|
||||
public class SsoCallbackController extends CallbackController {
|
||||
|
||||
private final SsoManager _ssoManager;
|
||||
private final Config _config;
|
||||
|
||||
@Inject
|
||||
public SsoCallbackController(
|
||||
@ -41,8 +46,10 @@ public class SsoCallbackController extends CallbackController {
|
||||
@Nonnull Authentication systemAuthentication,
|
||||
@Nonnull SystemEntityClient entityClient,
|
||||
@Nonnull AuthServiceClient authClient,
|
||||
@Nonnull Config config,
|
||||
@Nonnull com.typesafe.config.Config configs) {
|
||||
_ssoManager = ssoManager;
|
||||
_config = config;
|
||||
setDefaultUrl("/"); // By default, redirects to Home Page on log in.
|
||||
setSaveInSession(false);
|
||||
setCallbackLogic(
|
||||
@ -126,7 +133,18 @@ public class SsoCallbackController extends CallbackController {
|
||||
}
|
||||
|
||||
private boolean shouldHandleCallback(final String protocol) {
|
||||
return _ssoManager.isSsoEnabled()
|
||||
&& _ssoManager.getSsoProvider().protocol().getCommonName().equals(protocol);
|
||||
if (!_ssoManager.isSsoEnabled()) {
|
||||
return false;
|
||||
}
|
||||
updateConfig();
|
||||
return _ssoManager.getSsoProvider().protocol().getCommonName().equals(protocol);
|
||||
}
|
||||
|
||||
private void updateConfig() {
|
||||
final Clients clients = new Clients();
|
||||
final List<Client> clientList = new ArrayList<>();
|
||||
clientList.add(_ssoManager.getSsoProvider().client());
|
||||
clients.setClients(clientList);
|
||||
_config.setClients(clients);
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +91,9 @@ public class ApplicationTest extends WithBrowser {
|
||||
@BeforeAll
|
||||
public void init() throws IOException {
|
||||
_gmsServer = new MockWebServer();
|
||||
_gmsServer.enqueue(new MockResponse().setResponseCode(404)); // dynamic settings - not tested
|
||||
_gmsServer.enqueue(new MockResponse().setResponseCode(404)); // dynamic settings - not tested
|
||||
_gmsServer.enqueue(new MockResponse().setResponseCode(404)); // dynamic settings - not tested
|
||||
_gmsServer.enqueue(new MockResponse().setBody(String.format("{\"value\":\"%s\"}", TEST_USER)));
|
||||
_gmsServer.enqueue(
|
||||
new MockResponse().setBody(String.format("{\"accessToken\":\"%s\"}", TEST_TOKEN)));
|
||||
|
||||
@ -311,7 +311,9 @@ public class OidcConfigurationTest {
|
||||
public void readTimeoutPropagation() {
|
||||
|
||||
CONFIG.withValue(OIDC_READ_TIMEOUT, ConfigValueFactory.fromAnyRef("10000"));
|
||||
OidcConfigs oidcConfigs = new OidcConfigs(CONFIG);
|
||||
OidcConfigs.Builder oidcConfigsBuilder = new OidcConfigs.Builder();
|
||||
oidcConfigsBuilder.from(CONFIG);
|
||||
OidcConfigs oidcConfigs = oidcConfigsBuilder.build();
|
||||
OidcProvider oidcProvider = new OidcProvider(oidcConfigs);
|
||||
assertEquals(10000, ((OidcClient) oidcProvider.client()).getConfiguration().getReadTimeout());
|
||||
}
|
||||
|
||||
@ -7,6 +7,11 @@ namespace com.linkedin.settings.global
|
||||
"name": "globalSettingsInfo"
|
||||
}
|
||||
record GlobalSettingsInfo {
|
||||
|
||||
/**
|
||||
* SSO integrations between DataHub and identity providers
|
||||
*/
|
||||
sso: optional SsoSettings
|
||||
/**
|
||||
* Settings related to the Views Feature
|
||||
*/
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
namespace com.linkedin.settings.global
|
||||
|
||||
/**
|
||||
* Settings for OIDC SSO integration.
|
||||
*/
|
||||
record OidcSettings {
|
||||
/**
|
||||
* Whether OIDC SSO is enabled.
|
||||
*/
|
||||
enabled: boolean
|
||||
|
||||
/**
|
||||
* Unique client id issued by the identity provider.
|
||||
*/
|
||||
clientId: string
|
||||
|
||||
/**
|
||||
* Unique client secret issued by the identity provider.
|
||||
*/
|
||||
clientSecret: string
|
||||
|
||||
/**
|
||||
* The IdP OIDC discovery url.
|
||||
*/
|
||||
discoveryUri: string
|
||||
|
||||
/**
|
||||
* ADVANCED. The attribute / claim used to derive the DataHub username. Defaults to "preferred_username".
|
||||
*/
|
||||
userNameClaim: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. TThe regex used to parse the DataHub username from the user name claim. Defaults to (.*) (all).
|
||||
*/
|
||||
userNameClaimRegex: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. String representing the requested scope from the IdP. Defaults to "oidc email profile".
|
||||
*/
|
||||
scope: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. Which authentication method to use to pass credentials (clientId and clientSecret) to the token endpoint: Defaults to "client_secret_basic".
|
||||
*/
|
||||
clientAuthenticationMethod: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. Whether DataHub users should be provisioned on login if they do not exist. Defaults to true.
|
||||
*/
|
||||
jitProvisioningEnabled: optional boolean
|
||||
|
||||
/**
|
||||
* ADVANCED. Whether the user should already exist in DataHub on login, failing login if they are not. Defaults to false.
|
||||
*/
|
||||
preProvisioningRequired: optional boolean
|
||||
|
||||
/**
|
||||
* ADVANCED. Whether groups should be extracted from a claim in the OIDC profile. Only applies if JIT provisioning is enabled. Groups will be created if they do not exist. Defaults to true.
|
||||
*/
|
||||
extractGroupsEnabled: optional boolean
|
||||
|
||||
/**
|
||||
* ADVANCED. The OIDC claim to extract groups information from. Defaults to 'groups'.
|
||||
*/
|
||||
groupsClaim: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. Response type.
|
||||
*/
|
||||
responseType: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. Response mode.
|
||||
*/
|
||||
responseMode: optional string
|
||||
|
||||
/**
|
||||
* ADVANCED. Use Nonce.
|
||||
*/
|
||||
useNonce: optional boolean
|
||||
|
||||
/**
|
||||
* ADVANCED. Read timeout.
|
||||
*/
|
||||
readTimeout: optional long
|
||||
|
||||
/**
|
||||
* ADVANCED. Whether to extract claims from JWT access token. Defaults to false.
|
||||
*/
|
||||
extractJwtAccessTokenClaims: optional boolean
|
||||
|
||||
/**
|
||||
* ADVANCED. Which jws algorithm to use.
|
||||
*/
|
||||
preferredJwsAlgorithm: optional string
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
namespace com.linkedin.settings.global
|
||||
|
||||
/**
|
||||
* SSO Integrations, supported on the UI.
|
||||
*/
|
||||
record SsoSettings {
|
||||
/**
|
||||
* Auth base URL.
|
||||
*/
|
||||
baseUrl: string
|
||||
|
||||
/**
|
||||
* Optional OIDC SSO settings.
|
||||
*/
|
||||
oidcSettings: optional OidcSettings
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
namespace com.linkedin.settings.global
|
||||
|
||||
/**
|
||||
* Enum to define SSO protocol type.
|
||||
*/
|
||||
enum SsoType {
|
||||
/**
|
||||
* OIDC SSO is used.
|
||||
*/
|
||||
OIDC
|
||||
}
|
||||
@ -17,6 +17,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.linkedin.common.urn.CorpuserUrn;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.gms.factory.config.ConfigurationProvider;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.secret.SecretService;
|
||||
import com.linkedin.settings.global.GlobalSettingsInfo;
|
||||
import com.linkedin.settings.global.OidcSettings;
|
||||
import com.linkedin.settings.global.SsoSettings;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
@ -48,6 +54,25 @@ public class AuthServiceController {
|
||||
private static final String ARE_NATIVE_USER_CREDENTIALS_RESET_FIELD_NAME =
|
||||
"areNativeUserCredentialsReset";
|
||||
private static final String DOES_PASSWORD_MATCH_FIELD_NAME = "doesPasswordMatch";
|
||||
private static final String BASE_URL = "baseUrl";
|
||||
private static final String OIDC_ENABLED = "oidcEnabled";
|
||||
private static final String CLIENT_ID = "clientId";
|
||||
private static final String CLIENT_SECRET = "clientSecret";
|
||||
private static final String DISCOVERY_URI = "discoveryUri";
|
||||
private static final String USER_NAME_CLAIM = "userNameClaim";
|
||||
private static final String USER_NAME_CLAIM_REGEX = "userNameClaimRegex";
|
||||
private static final String SCOPE = "scope";
|
||||
private static final String CLIENT_AUTHENTICATION_METHOD = "clientAuthenticationMethod";
|
||||
private static final String JIT_PROVISIONING_ENABLED = "jitProvisioningEnabled";
|
||||
private static final String PRE_PROVISIONING_REQUIRED = "preProvisioningRequired";
|
||||
private static final String EXTRACT_GROUPS_ENABLED = "extractGroupsEnabled";
|
||||
private static final String GROUPS_CLAIM = "groupsClaim";
|
||||
private static final String RESPONSE_TYPE = "responseType";
|
||||
private static final String RESPONSE_MODE = "responseMode";
|
||||
private static final String USE_NONCE = "useNonce";
|
||||
private static final String READ_TIMEOUT = "readTimeout";
|
||||
private static final String EXTRACT_JWT_ACCESS_TOKEN_CLAIMS = "extractJwtAccessTokenClaims";
|
||||
private static final String PREFERRED_JWS_ALGORITHM = "preferredJwsAlgorithm";
|
||||
|
||||
@Inject StatelessTokenService _statelessTokenService;
|
||||
|
||||
@ -59,6 +84,10 @@ public class AuthServiceController {
|
||||
|
||||
@Inject NativeUserService _nativeUserService;
|
||||
|
||||
@Inject EntityService _entityService;
|
||||
|
||||
@Inject SecretService _secretService;
|
||||
|
||||
@Inject InviteTokenService _inviteTokenService;
|
||||
|
||||
@Inject @Nullable TrackingService _trackingService;
|
||||
@ -361,6 +390,41 @@ public class AuthServiceController {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets possible SSO settings.
|
||||
*
|
||||
* <p>Example Request:
|
||||
*
|
||||
* <p>POST /getSsoSettings -H "Authorization: Basic <system-client-id>:<system-client-secret>" {
|
||||
* "userUrn": "urn:li:corpuser:test" "password": "password123" }
|
||||
*
|
||||
* <p>Example Response:
|
||||
*
|
||||
* <p>{ "clientId": "clientId", "clientSecret": "secret", "discoveryUri = "discoveryUri" }
|
||||
*/
|
||||
@PostMapping(value = "/getSsoSettings", produces = "application/json;charset=utf-8")
|
||||
CompletableFuture<ResponseEntity<String>> getSsoSettings(final HttpEntity<String> httpEntity) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
GlobalSettingsInfo globalSettingsInfo =
|
||||
(GlobalSettingsInfo)
|
||||
_entityService.getLatestAspect(
|
||||
GLOBAL_SETTINGS_URN, GLOBAL_SETTINGS_INFO_ASPECT_NAME);
|
||||
if (globalSettingsInfo == null || !globalSettingsInfo.hasSso()) {
|
||||
log.debug("There are no SSO settings available");
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
SsoSettings ssoSettings =
|
||||
Objects.requireNonNull(globalSettingsInfo.getSso(), "ssoSettings cannot be null");
|
||||
String response = buildSsoSettingsResponse(ssoSettings);
|
||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Currently, only internal system is authorized to generate a token on behalf of a user!
|
||||
private boolean isAuthorizedToGenerateSessionToken(final String actorId) {
|
||||
// Verify that the actor is an internal system caller.
|
||||
@ -391,4 +455,67 @@ public class AuthServiceController {
|
||||
json.put(DOES_PASSWORD_MATCH_FIELD_NAME, doesPasswordMatch);
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
private String buildSsoSettingsResponse(final SsoSettings ssoSettings) {
|
||||
String baseUrl = Objects.requireNonNull(ssoSettings.getBaseUrl());
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(BASE_URL, baseUrl);
|
||||
|
||||
if (ssoSettings.hasOidcSettings()) {
|
||||
OidcSettings oidcSettings =
|
||||
Objects.requireNonNull(ssoSettings.getOidcSettings(), "oidcSettings cannot be null");
|
||||
buildOidcSettingsResponse(json, oidcSettings);
|
||||
}
|
||||
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
private void buildOidcSettingsResponse(JSONObject json, final OidcSettings oidcSettings) {
|
||||
json.put(OIDC_ENABLED, oidcSettings.isEnabled());
|
||||
json.put(CLIENT_ID, oidcSettings.getClientId());
|
||||
json.put(CLIENT_SECRET, _secretService.decrypt(oidcSettings.getClientSecret()));
|
||||
json.put(DISCOVERY_URI, oidcSettings.getDiscoveryUri());
|
||||
if (oidcSettings.hasUserNameClaim()) {
|
||||
json.put(USER_NAME_CLAIM, oidcSettings.getUserNameClaim());
|
||||
}
|
||||
if (oidcSettings.hasUserNameClaimRegex()) {
|
||||
json.put(USER_NAME_CLAIM_REGEX, oidcSettings.getUserNameClaimRegex());
|
||||
}
|
||||
if (oidcSettings.hasScope()) {
|
||||
json.put(SCOPE, oidcSettings.getScope());
|
||||
}
|
||||
if (oidcSettings.hasClientAuthenticationMethod()) {
|
||||
json.put(CLIENT_AUTHENTICATION_METHOD, oidcSettings.getClientAuthenticationMethod());
|
||||
}
|
||||
if (oidcSettings.hasJitProvisioningEnabled()) {
|
||||
json.put(JIT_PROVISIONING_ENABLED, oidcSettings.isJitProvisioningEnabled());
|
||||
}
|
||||
if (oidcSettings.hasPreProvisioningRequired()) {
|
||||
json.put(PRE_PROVISIONING_REQUIRED, oidcSettings.isPreProvisioningRequired());
|
||||
}
|
||||
if (oidcSettings.hasExtractGroupsEnabled()) {
|
||||
json.put(EXTRACT_GROUPS_ENABLED, oidcSettings.isExtractGroupsEnabled());
|
||||
}
|
||||
if (oidcSettings.hasGroupsClaim()) {
|
||||
json.put(GROUPS_CLAIM, oidcSettings.getGroupsClaim());
|
||||
}
|
||||
if (oidcSettings.hasResponseType()) {
|
||||
json.put(RESPONSE_TYPE, oidcSettings.getResponseType());
|
||||
}
|
||||
if (oidcSettings.hasResponseMode()) {
|
||||
json.put(RESPONSE_MODE, oidcSettings.getResponseMode());
|
||||
}
|
||||
if (oidcSettings.hasUseNonce()) {
|
||||
json.put(USE_NONCE, oidcSettings.isUseNonce());
|
||||
}
|
||||
if (oidcSettings.hasReadTimeout()) {
|
||||
json.put(READ_TIMEOUT, oidcSettings.getReadTimeout());
|
||||
}
|
||||
if (oidcSettings.hasExtractJwtAccessTokenClaims()) {
|
||||
json.put(EXTRACT_JWT_ACCESS_TOKEN_CLAIMS, oidcSettings.isExtractJwtAccessTokenClaims());
|
||||
}
|
||||
if (oidcSettings.hasPreferredJwsAlgorithm()) {
|
||||
json.put(PREFERRED_JWS_ALGORITHM, oidcSettings.getPreferredJwsAlgorithm());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user