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

feat(sso): Just-In-Time User & Group Provisioning on SSO Login (oidc) #3082

Merged
merged 22 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ project.ext.externalDependency = [
'playJavaJdbc': 'com.typesafe.play:play-java-jdbc_2.11:2.6.18',
'playTest': 'com.typesafe.play:play-test_2.11:2.6.18',
'pac4j': 'org.pac4j:pac4j-oidc:3.6.0',
'playPac4j': 'org.pac4j:play-pac4j_2.11:7.0.0',
'playPac4j': 'org.pac4j:play-pac4j_2.11:7.0.1',
'postgresql': 'org.postgresql:postgresql:42.2.14',
'reflections': 'org.reflections:reflections:0.9.11',
'rythmEngine': 'org.rythmengine:rythm-engine:1.3.0',
Expand Down
148 changes: 34 additions & 114 deletions datahub-frontend/app/react/auth/AuthModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,36 @@
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.linkedin.common.urn.CorpuserUrn;
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.core.engine.DefaultCallbackLogic;
import org.pac4j.core.http.adapter.HttpActionAdapter;
import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.play.CallbackController;
import org.pac4j.play.LogoutController;
import org.pac4j.play.PlayWebContext;
import org.pac4j.play.http.PlayHttpActionAdapter;
import org.pac4j.play.store.PlayCookieSessionStore;
import org.pac4j.play.store.PlaySessionStore;
import play.Environment;
import play.mvc.Result;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import react.auth.sso.oidc.OidcProvider;
import react.auth.sso.oidc.OidcConfigs;
import react.auth.sso.SsoConfigs;
import react.auth.sso.SsoManager;
import react.controllers.SsoCallbackController;

import static react.auth.sso.oidc.OidcConfigs.*;

import static react.auth.AuthUtils.*;

/**
* Responsible for configuring, validating, and providing authentication related components.
*/
public class AuthModule extends AbstractModule {

private static final String AUTH_BASE_URL_CONFIG_PATH = "auth.baseUrl";
private static final String AUTH_BASE_CALLBACK_PATH_CONFIG_PATH = "auth.baseCallbackPath";
private static final String AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH = "auth.successRedirectPath";

private static final String DEFAULT_BASE_CALLBACK_PATH = "/callback";
private static final String DEFAULT_SUCCESS_REDIRECT_PATH = "/";

private String _authBaseUrl;
private String _authBaseCallbackPath;
private String _authSuccessRedirectPath;

private final com.typesafe.config.Config _configs;

/**
* OIDC-specific configurations.
*/
private final OidcConfigs _oidcConfigs;

public AuthModule(final Environment environment, final com.typesafe.config.Config configs) {
_configs = configs;
_oidcConfigs = new OidcConfigs(configs);

if (isIndirectAuthEnabled()) {
_authBaseUrl = configs.getString(AUTH_BASE_URL_CONFIG_PATH);
_authBaseCallbackPath = configs.hasPath(AUTH_BASE_CALLBACK_PATH_CONFIG_PATH)
? configs.getString(AUTH_BASE_CALLBACK_PATH_CONFIG_PATH)
: DEFAULT_BASE_CALLBACK_PATH;
_authSuccessRedirectPath = configs.hasPath(AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH)
? configs.getString(AUTH_SUCCESS_REDIRECT_PATH_CONFIG_PATH)
: DEFAULT_SUCCESS_REDIRECT_PATH;
}
}

@Override
Expand All @@ -75,100 +41,54 @@ protected void configure() {
bind(SessionStore.class).toInstance(playCacheCookieStore);
bind(PlaySessionStore.class).toInstance(playCacheCookieStore);

final CallbackController callbackController = new CallbackController() {};
callbackController.setDefaultUrl(_authSuccessRedirectPath);
callbackController.setCallbackLogic(new DefaultCallbackLogic<Result, PlayWebContext>() {
@Override
public Result perform(final PlayWebContext context, final Config config, final HttpActionAdapter<Result, PlayWebContext> httpActionAdapter,
final String inputDefaultUrl, final Boolean inputSaveInSession, final Boolean inputMultiProfile,
final Boolean inputRenewSession, final String client) {
final Result result = super.perform(context, config, httpActionAdapter, inputDefaultUrl, inputSaveInSession, inputMultiProfile, inputRenewSession, client);
if (_oidcConfigs.getClientName().equals(client)) {
return handleOidcCallback(result, context, getProfileManager(context, config));
}
throw new RuntimeException(String.format("Unrecognized client with name %s provided to callback URL.", client));
}
});
// Make OIDC the default SSO client.
if (_oidcConfigs.isOidcEnabled()) {
callbackController.setDefaultClient(_oidcConfigs.getClientName());
try {
bind(SsoCallbackController.class).toConstructor(SsoCallbackController.class.getConstructor(
react.auth.sso.SsoManager.class));
} catch (NoSuchMethodException | SecurityException e) {
System.out.println("Required constructor missing");
}
bind(CallbackController.class).toInstance(callbackController);
// logout
final LogoutController logoutController = new LogoutController();
logoutController.setDefaultUrl("/");
bind(LogoutController.class).toInstance(logoutController);
}

@Provides @Singleton
protected Config provideConfig() {
if (isIndirectAuthEnabled()) {

final Clients clients = new Clients(_authBaseUrl + _authBaseCallbackPath);
protected Config provideConfig(react.auth.sso.SsoManager ssoManager) {
if (ssoManager.isSsoEnabled()) {
final Clients clients = new Clients();
final List<Client> clientList = new ArrayList<>();

if (_oidcConfigs.isOidcEnabled()) {
final OidcConfiguration oidcConfiguration = new OidcConfiguration();
oidcConfiguration.setClientId(_oidcConfigs.getClientId());
oidcConfiguration.setSecret(_oidcConfigs.getClientSecret());
oidcConfiguration.setDiscoveryURI(_oidcConfigs.getDiscoveryUri());
oidcConfiguration.setClientAuthenticationMethodAsString(_oidcConfigs.getClientAuthenticationMethod());
oidcConfiguration.setScope(_oidcConfigs.getScope());

final OidcClient oidcClient = new OidcClient(oidcConfiguration);
oidcClient.setName(_oidcConfigs.getClientName());
oidcClient.setCallbackUrlResolver(new PathParameterCallbackUrlResolver());
clientList.add(oidcClient);
}
clientList.add(ssoManager.getSsoProvider().client());
clients.setClients(clientList);

final Config config = new Config(clients);
config.setHttpActionAdapter(new PlayHttpActionAdapter());
return config;
}
return new Config();
}

private Result handleOidcCallback(final Result result, final PlayWebContext context, ProfileManager<CommonProfile> profileManager) {
if (OidcResponseErrorHandler.isError(context)) {
return OidcResponseErrorHandler.handleError(context);
}
else if (profileManager.isAuthenticated() && profileManager.get(true).isPresent()) {
final CommonProfile profile = profileManager.get(true).get();
if (!profile.containsAttribute(_oidcConfigs.getUserNameClaim())) {
throw new RuntimeException(
String.format(
"Failed to resolve user name claim from profile provided by Identity Provider. Missing attribute '%s'",
_oidcConfigs.getUserNameClaim()
));
}

final String userNameClaim = (String) profile.getAttribute(_oidcConfigs.getUserNameClaim());
final Pattern pattern = Pattern.compile(_oidcConfigs.getUserNameClaimRegex());
final Matcher matcher = pattern.matcher(userNameClaim);
if (matcher.find()) {
final String userName = matcher.group();
final String actorUrn = new CorpuserUrn(userName).toString();
context.getJavaSession().put(ACTOR, actorUrn);
return result.withCookies(createActorCookie(actorUrn, _configs.hasPath(SESSION_TTL_CONFIG_PATH)
? _configs.getInt(SESSION_TTL_CONFIG_PATH)
: DEFAULT_SESSION_TTL_HOURS));
} else {
throw new RuntimeException(
String.format("Failed to extract DataHub username from username claim %s using regex %s",
userNameClaim,
_oidcConfigs.getUserNameClaimRegex()));
@Provides @Singleton
protected react.auth.sso.SsoManager provideSsoManager() {
react.auth.sso.SsoManager manager = new SsoManager();
// Seed the SSO manager with a default SSO provider.
if (isSsoEnabled(_configs)) {
react.auth.sso.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);
}
}
throw new RuntimeException(String.format("Failed to authenticate current user. Cannot find valid identity provider profile in session"));
return manager;
}

/**
* Returns true if indirect authentication is enabled (callback-based SSO), false otherwise.
* Currently, only OIDC is supported.
*/
private boolean isIndirectAuthEnabled() {
return _oidcConfigs.isOidcEnabled();
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)));
}
}

27 changes: 27 additions & 0 deletions datahub-frontend/app/react/auth/ConfigUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package react.auth;

public class ConfigUtil {

private ConfigUtil() { }

public static String getRequired(
final com.typesafe.config.Config configs,
final String path) {
if (!configs.hasPath(path)) {
throw new IllegalArgumentException(
String.format("Missing required config with path %s", path));
}
return configs.getString(path);
}

public static String getOptional
(final com.typesafe.config.Config configs,
final String path,
final String defaultVal) {
if (!configs.hasPath(path)) {
return defaultVal;
}
return configs.getString(path);
}

}
135 changes: 0 additions & 135 deletions datahub-frontend/app/react/auth/OidcConfigs.java

This file was deleted.

Loading