diff --git a/src/prerna/semoss/web/services/local/UserResource.java b/src/prerna/semoss/web/services/local/UserResource.java index e6845c77..e8cd6a04 100644 --- a/src/prerna/semoss/web/services/local/UserResource.java +++ b/src/prerna/semoss/web/services/local/UserResource.java @@ -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; @@ -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 = """ + + + + + Signed in + + +

You are signed in. You can close this window.

+ + + + """; private static SocialPropertiesUtil socialData = null; static { @@ -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 @@ -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. * @@ -636,7 +723,7 @@ public Response loginJiraDynamic(@Context HttpServletRequest request, @Context H return null; } - setMainPageRedirect(request, response); + finishOAuthLogin(request, response); return null; } @@ -880,7 +967,7 @@ public Response loginSalesforceDynamic(@Context HttpServletRequest request, @Con return null; } - setMainPageRedirect(request, response); + finishOAuthLogin(request, response); return null; } @@ -1025,7 +1112,7 @@ public Response loginServiceNowDynamic(@Context HttpServletRequest request, @Con return null; } - setMainPageRedirect(request, response); + finishOAuthLogin(request, response); return null; } @@ -1185,7 +1272,7 @@ public Response loginGeneric(@PathParam("provider") String provider, @Context Ht return null; } - setMainPageRedirect(request, response); + finishOAuthLogin(request, response); return null; } @@ -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); @@ -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); + } + ////////////////////////////////////////////////////////////////////// /**