package controllers; import static auth.AuthUtils.*; import static org.pac4j.core.client.IndirectClient.ATTEMPTED_AUTHENTICATION_SUFFIX; import static org.pac4j.play.store.PlayCookieSessionStore.*; import static utils.FrontendConstants.FALLBACK_LOGIN; import static utils.FrontendConstants.GUEST_LOGIN; import static utils.FrontendConstants.PASSWORD_LOGIN; import static utils.FrontendConstants.PASSWORD_RESET; import static utils.FrontendConstants.SIGN_UP_LINK_LOGIN; import auth.AuthUtils; import auth.CookieConfigs; import auth.GuestAuthenticationConfigs; import auth.JAASConfigs; import auth.NativeAuthenticationConfigs; import auth.sso.SsoManager; import client.AuthServiceClient; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; import com.linkedin.common.urn.CorpuserUrn; import com.linkedin.common.urn.Urn; import com.typesafe.config.Config; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; import java.util.Optional; import javax.annotation.Nonnull; import javax.inject.Inject; import org.apache.commons.httpclient.InvalidRedirectLocationException; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.client.Client; import org.pac4j.core.context.CallContext; import org.pac4j.core.context.WebContext; import org.pac4j.core.exception.http.FoundAction; import org.pac4j.core.exception.http.RedirectionAction; import org.pac4j.play.PlayWebContext; import org.pac4j.play.http.PlayHttpActionAdapter; import org.pac4j.play.store.PlayCookieSessionStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import play.data.validation.Constraints; import play.libs.Json; import play.mvc.Controller; import play.mvc.Http; import play.mvc.Result; import play.mvc.Results; import security.AuthenticationManager; public class AuthenticationController extends Controller { public static final String AUTH_VERBOSE_LOGGING = "auth.verbose.logging"; private static final String AUTH_REDIRECT_URI_PARAM = "redirect_uri"; private static final String ERROR_MESSAGE_URI_PARAM = "error_msg"; private static final String SSO_DISABLED_ERROR_MESSAGE = "SSO is not configured"; private static final String SSO_NO_REDIRECT_MESSAGE = "SSO is configured, however missing redirect from idp"; private static final Logger logger = LoggerFactory.getLogger(AuthenticationController.class.getName()); private final CookieConfigs cookieConfigs; private final JAASConfigs jaasConfigs; private final NativeAuthenticationConfigs nativeAuthenticationConfigs; private final GuestAuthenticationConfigs guestAuthenticationConfigs; private final boolean verbose; @Inject private org.pac4j.core.config.Config ssoConfig; @VisibleForTesting @Inject protected PlayCookieSessionStore playCookieSessionStore; @VisibleForTesting @Inject protected SsoManager ssoManager; @Inject AuthServiceClient authClient; @Inject public AuthenticationController(@Nonnull Config configs) { cookieConfigs = new CookieConfigs(configs); jaasConfigs = new JAASConfigs(configs); nativeAuthenticationConfigs = new NativeAuthenticationConfigs(configs); guestAuthenticationConfigs = new GuestAuthenticationConfigs(configs); verbose = configs.hasPath(AUTH_VERBOSE_LOGGING) && configs.getBoolean(AUTH_VERBOSE_LOGGING); } /** * Route used to perform authentication, or redirect to log in if authentication fails. * *
If indirect SSO (eg. oidc) is configured, this route will redirect to the identity provider
* (Indirect auth). If not, we will fall back to the default username / password login experience
* (Direct auth).
*/
@Nonnull
public Result authenticate(Http.Request request) {
// TODO: Call getAuthenticatedUser and then generate a session cookie for the UI if the user is
// authenticated.
final Optional TODO: Implement built-in support for LDAP auth. Currently dummy jaas authentication is the
* default.
*/
@Nonnull
public Result logIn(Http.Request request) {
boolean jaasEnabled = jaasConfigs.isJAASEnabled();
logger.debug(String.format("Jaas authentication enabled: %b", jaasEnabled));
boolean nativeAuthenticationEnabled =
nativeAuthenticationConfigs.isNativeAuthenticationEnabled();
logger.debug(String.format("Native authentication enabled: %b", nativeAuthenticationEnabled));
boolean noAuthEnabled = !jaasEnabled && !nativeAuthenticationEnabled;
if (noAuthEnabled) {
String message = "Neither JAAS nor native authentication is enabled on the server.";
final ObjectNode error = Json.newObject();
error.put("message", message);
return Results.badRequest(error);
}
final JsonNode json = request.body().asJson();
final String username = json.findPath(USER_NAME).textValue();
final String password = json.findPath(PASSWORD).textValue();
if (StringUtils.isBlank(username)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "User name must not be empty.");
return Results.badRequest(invalidCredsJson);
}
JsonNode invalidCredsJson = Json.newObject().put("message", "Invalid Credentials");
boolean loginSucceeded = tryLogin(username, password);
if (!loginSucceeded) {
logger.info("Login failed for user: {}", username);
return Results.badRequest(invalidCredsJson);
}
final Urn actorUrn = new CorpuserUrn(username);
logger.info("Login successful for user: {}, urn: {}", username, actorUrn);
final String accessToken =
authClient.generateSessionTokenForUser(actorUrn.getId(), PASSWORD_LOGIN);
return createSession(actorUrn.toString(), accessToken);
}
/**
* Sign up a native user based on a name, email, title, and password. The invite token must match
* an existing invite token.
*/
@Nonnull
public Result signUp(Http.Request request) {
boolean nativeAuthenticationEnabled =
nativeAuthenticationConfigs.isNativeAuthenticationEnabled();
logger.debug(String.format("Native authentication enabled: %b", nativeAuthenticationEnabled));
if (!nativeAuthenticationEnabled) {
String message = "Native authentication is not enabled on the server.";
final ObjectNode error = Json.newObject();
error.put("message", message);
return Results.badRequest(error);
}
final JsonNode json = request.body().asJson();
final String fullName = json.findPath(FULL_NAME).textValue();
final String email = json.findPath(EMAIL).textValue();
final String title = json.findPath(TITLE).textValue();
final String password = json.findPath(PASSWORD).textValue();
final String inviteToken = json.findPath(INVITE_TOKEN).textValue();
if (StringUtils.isBlank(fullName)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Full name must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (StringUtils.isBlank(email)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (nativeAuthenticationConfigs.isEnforceValidEmailEnabled()) {
Constraints.EmailValidator emailValidator = new Constraints.EmailValidator();
if (!emailValidator.isValid(email)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty.");
return Results.badRequest(invalidCredsJson);
}
}
if (StringUtils.isBlank(password)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (StringUtils.isBlank(title)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Title must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (StringUtils.isBlank(inviteToken)) {
JsonNode invalidCredsJson =
Json.newObject().put("message", "Invite token must not be empty.");
return Results.badRequest(invalidCredsJson);
}
final Urn userUrn = new CorpuserUrn(email);
final String userUrnString = userUrn.toString();
authClient.signUp(userUrnString, fullName, email, title, password, inviteToken);
logger.info("Signed up user {} using invite tokens", userUrnString);
final String accessToken =
authClient.generateSessionTokenForUser(userUrn.getId(), SIGN_UP_LINK_LOGIN);
return createSession(userUrnString, accessToken);
}
/** Reset a native user's credentials based on a username, old password, and new password. */
@Nonnull
public Result resetNativeUserCredentials(Http.Request request) {
boolean nativeAuthenticationEnabled =
nativeAuthenticationConfigs.isNativeAuthenticationEnabled();
logger.debug(String.format("Native authentication enabled: %b", nativeAuthenticationEnabled));
if (!nativeAuthenticationEnabled) {
String message = "Native authentication is not enabled on the server.";
final ObjectNode error = Json.newObject();
error.put("message", message);
return badRequest(error);
}
final JsonNode json = request.body().asJson();
final String email = json.findPath(EMAIL).textValue();
final String password = json.findPath(PASSWORD).textValue();
final String resetToken = json.findPath(RESET_TOKEN).textValue();
if (StringUtils.isBlank(email)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (StringUtils.isBlank(password)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty.");
return Results.badRequest(invalidCredsJson);
}
if (StringUtils.isBlank(resetToken)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Reset token must not be empty.");
return Results.badRequest(invalidCredsJson);
}
final Urn userUrn = new CorpuserUrn(email);
final String userUrnString = userUrn.toString();
authClient.resetNativeUserCredentials(userUrnString, password, resetToken);
final String accessToken =
authClient.generateSessionTokenForUser(userUrn.getId(), PASSWORD_RESET);
return createSession(userUrnString, accessToken);
}
@VisibleForTesting
protected Result addRedirectCookie(Result result, CallContext ctx, String redirectPath) {
// Set the originally requested path for post-auth redirection. We split off into a separate
// cookie from the session
// to reduce size of the session cookie
FoundAction foundAction = new FoundAction(redirectPath);
byte[] javaSerBytes =
((PlayCookieSessionStore) ctx.sessionStore()).getSerializer().serializeToBytes(foundAction);
String serialized = Base64.getEncoder().encodeToString(compressBytes(javaSerBytes));
Http.CookieBuilder redirectCookieBuilder =
Http.Cookie.builder(REDIRECT_URL_COOKIE_NAME, serialized);
redirectCookieBuilder.withPath("/");
redirectCookieBuilder.withSecure(true);
redirectCookieBuilder.withHttpOnly(true);
redirectCookieBuilder.withMaxAge(Duration.ofSeconds(86400));
redirectCookieBuilder.withSameSite(Http.Cookie.SameSite.NONE);
return result.withCookies(redirectCookieBuilder.build());
}
private Optional