Skip to content
Merged
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
126 changes: 111 additions & 15 deletions src/prerna/semoss/web/services/local/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -104,6 +106,28 @@ public class UserResource {
private static final Logger classLogger = LogManager.getLogger(UserResource.class);

private static final String CUSTOM_REDIRECT_SESSION_KEY = "custom_redirect";
private static final String OAUTH_DISABLE_REDIRECT_SESSION_KEY = "oauth_disable_redirect";

private static final SecureRandom NONCE_RANDOM = new SecureRandom();

/**
* What a login started with {@code disableRedirect=true} ends on instead of a
* redirect: the window the sign in ran in closes itself. The FE that opened it
* also closes it once it sees the login, so the page works without its script.
*/
private static final String LOGIN_COMPLETE_PAGE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Signed in</title>
</head>
<body>
<p>You are signed in. You can close this window.</p>
<script nonce="%s">window.close();</script>
</body>
</html>
""";

private static SocialPropertiesUtil socialData = null;
static {
Expand Down Expand Up @@ -400,7 +424,8 @@ public Response userinfoGeneric(@PathParam("provider") String provider, @Context

/**
* Initializes an OAuth login flow session and stores a custom redirect value
* when present.
* when present, and whether the login should end without a redirect
* ({@code disableRedirect=true}).
*
* @param request inbound HTTP request
* @return existing or newly-created session
Expand All @@ -414,9 +439,71 @@ private HttpSession initializeOAuthLoginSession(HttpServletRequest request) {
}
session.setAttribute(CUSTOM_REDIRECT_SESSION_KEY, customRedirect);
}

// the provider's callback does not carry the parameters the login started
// with, so disableRedirect is kept in the session until the login ends. A
// login that starts without it drops one an abandoned login left behind.
if (!isOAuthCallback(request)) {
if (Boolean.parseBoolean(request.getParameter("disableRedirect"))) {
if (session == null) {
session = request.getSession();
}
session.setAttribute(OAUTH_DISABLE_REDIRECT_SESSION_KEY, Boolean.TRUE);
} else if (session != null) {
session.removeAttribute(OAUTH_DISABLE_REDIRECT_SESSION_KEY);
}
}
return session;
}

/**
* Whether a request to an OAuth login endpoint is the provider sending the user
* back, rather than the FE starting a login.
*
* @param request inbound HTTP request
* @return true for a provider callback
*/
private boolean isOAuthCallback(HttpServletRequest request) {
return request.getParameter("code") != null || request.getParameter("state") != null
|| request.getParameter("error") != null;
}

/**
* Ends a successful OAuth login. A login started with
* {@code disableRedirect=true}, such as one the FE opened in a popup, ends on a
* page that closes its window; every other login is redirected to the main page
* or its custom redirect.
*
* @param request inbound HTTP request
* @param response outbound HTTP response
* @throws IOException when the response cannot be written
*/
private void finishOAuthLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
if (!Boolean.TRUE.equals(session.getAttribute(OAUTH_DISABLE_REDIRECT_SESSION_KEY))) {
setMainPageRedirect(request, response);
return;
}

session.removeAttribute(OAUTH_DISABLE_REDIRECT_SESSION_KEY);
session.removeAttribute(CUSTOM_REDIRECT_SESSION_KEY);
addSessionCookieHeader(request, response, session);

byte[] nonceBytes = new byte[16];
NONCE_RANDOM.nextBytes(nonceBytes);
String nonce = Base64.getEncoder().encodeToString(nonceBytes);

response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Cache-Control", "no-store");
// its own policy: only this page's one script may run
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'nonce-" + nonce
+ "'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
response.getWriter().write(LOGIN_COMPLETE_PAGE.formatted(nonce));
// commit it here, since the resource method returns null
response.flushBuffer();
}

/**
* Resolves the authenticated user from a session.
*
Expand Down Expand Up @@ -636,7 +723,7 @@ public Response loginJiraDynamic(@Context HttpServletRequest request, @Context H
return null;
}

setMainPageRedirect(request, response);
finishOAuthLogin(request, response);
return null;
}

Expand Down Expand Up @@ -880,7 +967,7 @@ public Response loginSalesforceDynamic(@Context HttpServletRequest request, @Con
return null;
}

setMainPageRedirect(request, response);
finishOAuthLogin(request, response);
return null;
}

Expand Down Expand Up @@ -1025,7 +1112,7 @@ public Response loginServiceNowDynamic(@Context HttpServletRequest request, @Con
return null;
}

setMainPageRedirect(request, response);
finishOAuthLogin(request, response);
return null;
}

Expand Down Expand Up @@ -1185,7 +1272,7 @@ public Response loginGeneric(@PathParam("provider") String provider, @Context Ht
return null;
}

setMainPageRedirect(request, response);
finishOAuthLogin(request, response);
return null;
}

Expand Down Expand Up @@ -1856,21 +1943,12 @@ private void setMainPageRedirect(@Context HttpServletRequest request, @Context H
// if so, we will send them back to that URL
// otherwise, we send them back to the FE
HttpSession session = request.getSession();
String contextPath = request.getContextPath();

boolean useCustom = customRedirect != null && !customRedirect.isEmpty();
boolean endpoint = session.getAttribute(Constants.ENDPOINT_REDIRECT_KEY) != null;
response.setStatus(302);
try {
boolean secureRequest = "https".equalsIgnoreCase(WebUtility.getProtocol(request));
// add the cookie to the header directly
// to allow for cross site login when embedded as iframe
String setCookieString = DBLoader.getSessionIdKey() + "=" + session.getId() + "; Path=" + contextPath
+ "; HttpOnly"
+ ((ClusterUtil.IS_CLUSTER || secureRequest)
? ("; Secure; SameSite=" + Utility.getSameSiteCookieValue())
: "");
response.addHeader("Set-Cookie", setCookieString);
addSessionCookieHeader(request, response, session);
if (useCustom) {
response.addHeader("redirect", customRedirect);
String encodedCustomRedirect = Encode.forHtml(customRedirect);
Expand All @@ -1887,6 +1965,24 @@ private void setMainPageRedirect(@Context HttpServletRequest request, @Context H
}
}

/**
* Adds the session cookie to the response directly, to allow for cross site
* login when the app is embedded as an iframe.
*
* @param request inbound HTTP request
* @param response outbound HTTP response
* @param session the logged in session
*/
private void addSessionCookieHeader(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
boolean secureRequest = "https".equalsIgnoreCase(WebUtility.getProtocol(request));
String setCookieString = DBLoader.getSessionIdKey() + "=" + session.getId() + "; Path="
+ request.getContextPath() + "; HttpOnly"
+ ((ClusterUtil.IS_CLUSTER || secureRequest)
? ("; Secure; SameSite=" + Utility.getSameSiteCookieValue())
: "");
response.addHeader("Set-Cookie", setCookieString);
}

//////////////////////////////////////////////////////////////////////

/**
Expand Down
Loading