package controllers;
import auth.CookieConfigs;
import auth.sso.SsoManager;
import auth.sso.SsoProvider;
import auth.sso.oidc.OidcCallbackLogic;
import client.AuthServiceClient;
import com.linkedin.entity.client.SystemEntityClient;
import io.datahubproject.metadata.context.OperationContext;
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 javax.inject.Named;
import lombok.extern.slf4j.Slf4j;
import org.pac4j.core.adapter.FrameworkAdapter;
import org.pac4j.core.client.Client;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.FrameworkParameters;
import org.pac4j.core.engine.CallbackLogic;
import org.pac4j.play.CallbackController;
import org.pac4j.play.context.PlayFrameworkParameters;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Results;
/**
* A dedicated Controller for handling redirects to DataHub by 3rd-party Identity Providers after
* off-platform authentication.
*
*
Handles a single "callback/{protocol}" route, where the protocol (ie. OIDC / SAML) determines
* the handling logic to invoke.
*/
@Slf4j
public class SsoCallbackController extends CallbackController {
private final SsoManager ssoManager;
private final Config config;
private final CallbackLogic callbackLogic;
@Inject
public SsoCallbackController(
@Nonnull SsoManager ssoManager,
@Named("systemOperationContext") @Nonnull OperationContext systemOperationContext,
@Nonnull SystemEntityClient entityClient,
@Nonnull AuthServiceClient authClient,
@Nonnull Config config,
@Nonnull com.typesafe.config.Config configs) {
this.ssoManager = ssoManager;
this.config = config;
setDefaultUrl("/"); // By default, redirects to Home Page on log in.
callbackLogic =
new SsoCallbackLogic(
ssoManager,
systemOperationContext,
entityClient,
authClient,
new CookieConfigs(configs));
}
@Override
public CompletionStage callback(Http.Request request) {
FrameworkAdapter.INSTANCE.applyDefaultSettingsIfUndefined(this.config);
return CompletableFuture.supplyAsync(
() -> {
return (Result)
callbackLogic.perform(
this.config,
getDefaultUrl(),
getRenewSession(),
getDefaultClient(),
new PlayFrameworkParameters(request));
},
this.ec.current());
}
public CompletionStage handleCallback(String protocol, Http.Request request) {
if (shouldHandleCallback(protocol)) {
log.debug(
"Handling SSO callback. Protocol: {}",
ssoManager.getSsoProvider().protocol().getCommonName());
return callback(request)
.handle(
(res, e) -> {
if (e != null) {
log.error(
"Caught exception while attempting to handle SSO callback! It's likely that SSO integration is mis-configured.",
e);
return Results.redirect(
String.format(
"/login?error_msg=%s",
URLEncoder.encode(
"Failed to sign in using Single Sign-On provider. Please try again, or contact your DataHub Administrator.",
StandardCharsets.UTF_8)))
.discardingCookie("actor")
.withNewSession();
}
return res;
});
}
return CompletableFuture.completedFuture(
Results.internalServerError(
String.format(
"Failed to perform SSO callback. SSO is not enabled for protocol: %s", protocol)));
}
/** Logic responsible for delegating to protocol-specific callback logic. */
public class SsoCallbackLogic implements CallbackLogic {
private final OidcCallbackLogic oidcCallbackLogic;
SsoCallbackLogic(
final SsoManager ssoManager,
final OperationContext systemOperationContext,
final SystemEntityClient entityClient,
final AuthServiceClient authClient,
final CookieConfigs cookieConfigs) {
oidcCallbackLogic =
new OidcCallbackLogic(
ssoManager, systemOperationContext, entityClient, authClient, cookieConfigs);
}
@Override
public Object perform(
Config config,
String inputDefaultUrl,
Boolean inputRenewSession,
String defaultClient,
FrameworkParameters parameters) {
if (SsoProvider.SsoProtocol.OIDC.equals(ssoManager.getSsoProvider().protocol())) {
return oidcCallbackLogic.perform(
config, inputDefaultUrl, inputRenewSession, defaultClient, parameters);
}
// Should never occur.
throw new UnsupportedOperationException(
"Failed to find matching SSO Provider. Only one supported is OIDC.");
}
}
private boolean shouldHandleCallback(final String protocol) {
if (!ssoManager.isSsoEnabled()) {
return false;
}
updateConfig();
return ssoManager.getSsoProvider().protocol().getCommonName().equals(protocol);
}
private void updateConfig() {
final Clients clients = new Clients();
final List clientList = new ArrayList<>();
clientList.add(ssoManager.getSsoProvider().client());
clients.setClients(clientList);
config.setClients(clients);
}
}