Skip to content

Commit

Permalink
feat(sso): Just-In-Time User & Group Provisioning on SSO Login (oidc) (
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored and gabe-lyons committed Aug 31, 2021
1 parent b560d20 commit 96b4471
Show file tree
Hide file tree
Showing 32 changed files with 974 additions and 277 deletions.
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

0 comments on commit 96b4471

Please sign in to comment.