diff --git a/api/src/org/labkey/api/security/AuthFilter.java b/api/src/org/labkey/api/security/AuthFilter.java index aac2863e5c5..966a7357d1b 100644 --- a/api/src/org/labkey/api/security/AuthFilter.java +++ b/api/src/org/labkey/api/security/AuthFilter.java @@ -199,14 +199,9 @@ else if (!AppProps.getInstance().isDevMode()) } if (null == user) - { - if (AppProps.getInstance().isOptionalFeatureEnabled(AppProps.EXPERIMENTAL_NO_GUESTS)) - user = User.nobody; - else - user = User.guest; - } + user = getGuestUser(); else - UserManager.updateRecentUser(user.isImpersonated() ? user.getImpersonatingUser() : user); // TODO: Sanity check this with Matt... treat impersonating admin as active, not impersonated user + UserManager.updateRecentUser(user.isImpersonated() ? user.getImpersonatingUser() : user); req = AuthenticatedRequest.create(req, user); @@ -262,7 +257,13 @@ private void addRandomHeader(HttpServletRequest req, HttpServletResponse resp) resp.addHeader("X-LK-NONCE", sb.toString()); } - + public static User getGuestUser() + { + if (AppProps.getInstance().isOptionalFeatureEnabled(AppProps.EXPERIMENTAL_NO_GUESTS)) + return User.nobody; + else + return User.guest; + } private boolean clearRequestAttributes(HttpServletRequest request) { diff --git a/api/src/org/labkey/api/security/AuthenticatedRequest.java b/api/src/org/labkey/api/security/AuthenticatedRequest.java index 690e1f5b0ab..26a91d18f72 100644 --- a/api/src/org/labkey/api/security/AuthenticatedRequest.java +++ b/api/src/org/labkey/api/security/AuthenticatedRequest.java @@ -55,15 +55,12 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -/** - * User: matthewb - * Date: Feb 5, 2009 - */ public class AuthenticatedRequest extends HttpServletRequestWrapper implements AutoCloseable { private static final Logger _log = LogManager.getLogger(AuthenticatedRequest.class); private final User _user; + private boolean _loggedIn; private HttpSession _session = null; @@ -76,11 +73,17 @@ public static AuthenticatedRequest create(@NotNull HttpServletRequest request, @ private AuthenticatedRequest(@NotNull HttpServletRequest request, @NotNull User user) { - super(request instanceof AuthenticatedRequest ? (HttpServletRequest)((AuthenticatedRequest)request).getRequest() : request); + super(request instanceof AuthenticatedRequest authRequest ? authRequest.getRequest() : request); _user = user; _loggedIn = !_user.isGuest(); } + @Override + public HttpServletRequest getRequest() + { + return (HttpServletRequest)super.getRequest(); + } + @Override public void close() { diff --git a/api/src/org/labkey/api/security/AuthenticationManager.java b/api/src/org/labkey/api/security/AuthenticationManager.java index d8054bb6cc5..74e20dba6e5 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -1619,6 +1619,11 @@ public URLHelper getRedirectURL() public static @NotNull AuthenticationResult handleAuthentication(HttpServletRequest request, Container c) + { + return handleAuthentication(request, c, true); + } + + public static @NotNull AuthenticationResult handleAuthentication(HttpServletRequest request, Container c, boolean setSession) { HttpSession session = request.getSession(true); PrimaryAuthenticationResult primaryAuthResult = AuthenticationManager.getPrimaryAuthenticationResult(session); @@ -1686,13 +1691,16 @@ public URLHelper getRedirectURL() LoginReturnProperties properties = getLoginReturnProperties(request); URLHelper url = getAfterLoginURL(c, properties, primaryAuthUser); - // Prep the new session and set the user & authentication-related attributes - session = SecurityManager.setAuthenticatedUser(request, primaryAuthResult.getResponse(), primaryAuthUser, true); - - if (session.isNew() && !primaryAuthUser.isGuest()) + if (setSession) { - // notify the websocket clients a new http session for the user has been started - NotificationService.get().sendServerEvent(primaryAuthUser.getUserId(), AuthNotify.LoggedIn); + // Prep the new session and set the user & authentication-related attributes + session = SecurityManager.setAuthenticatedUser(request, primaryAuthResult.getResponse(), primaryAuthUser, true); + + if (session.isNew() && !primaryAuthUser.isGuest()) + { + // notify the websocket clients a new http session for the user has been started + NotificationService.get().sendServerEvent(primaryAuthUser.getUserId(), AuthNotify.LoggedIn); + } } // Set the authentication validators into the new session @@ -1759,6 +1767,34 @@ public static URLHelper getWelcomeURL() return new URLHelper(true); } + public record Reauth(String token, User user){} + public static final String REAUTH_TOKEN_NAME = "reauthToken"; + + public static @Nullable User getAndClearReauthUser(HttpServletRequest request, @Nullable String token) + { + if (token != null) + { + HttpSession session = request.getSession(false); + + if (session != null) + { + Reauth reauth = (Reauth) session.getAttribute(REAUTH_TOKEN_NAME); + + if (reauth != null) + { + boolean matches = token.equals(reauth.token()); + + if (matches) + { + session.removeAttribute(REAUTH_TOKEN_NAME); + return reauth.user(); + } + } + } + } + + return null; + } // test() method should return true if the authentication is still valid public interface AuthenticationValidator extends Predicate diff --git a/api/src/org/labkey/api/security/LoginUrls.java b/api/src/org/labkey/api/security/LoginUrls.java index 3a7652fb9c0..aa604fae023 100644 --- a/api/src/org/labkey/api/security/LoginUrls.java +++ b/api/src/org/labkey/api/security/LoginUrls.java @@ -35,6 +35,7 @@ public interface LoginUrls extends UrlProvider ActionURL getLoginURL(URLHelper returnUrl); ActionURL getRegisterURL(Container c, @Nullable URLHelper returnUrl); ActionURL getLoginURL(Container c, @Nullable URLHelper returnUrl); + ActionURL getForceReauthURL(Container c, @Nullable URLHelper returnUrl); ActionURL getLogoutURL(Container c); ActionURL getLogoutURL(Container c, URLHelper returnUrl); ActionURL getStopImpersonatingURL(Container c, @Nullable URLHelper returnUrl); diff --git a/api/src/org/labkey/api/view/RedirectException.java b/api/src/org/labkey/api/view/RedirectException.java index 6d832af899a..58519493062 100644 --- a/api/src/org/labkey/api/view/RedirectException.java +++ b/api/src/org/labkey/api/view/RedirectException.java @@ -23,8 +23,8 @@ /** * When thrown in the context of an HTTP request, sends the client a *temporary* redirect in the HTTP response. Not * treated as a loggable error. See {@link PermanentRedirectException} if a permanent redirect is desired. - * Note: This always redirects to the local server. If an external redirect is needed (this is rare), use - * {@link ExternalRedirectException} or (even rarer) {@link UnsafeExternalRedirectException}. + * Note: Instances of this specific class always redirect to the local server. If an external redirect is needed (this + * is rare), use {@link ExternalRedirectException} or (even rarer) {@link UnsafeExternalRedirectException}. */ public class RedirectException extends RuntimeException implements SkipMothershipLogging { diff --git a/api/src/org/labkey/api/view/UnauthorizedException.java b/api/src/org/labkey/api/view/UnauthorizedException.java index 564f0264ec1..d0ebd903d46 100644 --- a/api/src/org/labkey/api/view/UnauthorizedException.java +++ b/api/src/org/labkey/api/view/UnauthorizedException.java @@ -15,11 +15,10 @@ */ package org.labkey.api.view; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; -import jakarta.servlet.http.HttpServletResponse; - /** * Signals to the HTTP client that the request is not authorized, via a 401 status code. */ @@ -30,7 +29,7 @@ public class UnauthorizedException extends HttpStatusException /** Options for how the client should be informed of not being allowed to see a resource */ public enum Type { - /** Redirect the browser to a different URL to render a login form */ + /** If user is guest, redirect the browser to the login page */ redirectToLogin, /** Send a 401, but signal that the server would accept HTTP BasicAuth credentials */ sendBasicAuth, diff --git a/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java b/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java index dc077525b6b..743f0594e14 100644 --- a/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java +++ b/api/src/org/labkey/filters/ContentSecurityPolicyFilter.java @@ -75,7 +75,6 @@ public class ContentSecurityPolicyFilter implements Filter private String _stashedTemplate = null; // We can't set this statically because the class is referenced before URLProviders are available - @SuppressWarnings("DataFlowIssue") private final String _reportingEndpointsHeaderValue = "csp-report=\"" + PageFlowUtil.urlProvider(AdminUrls.class).getCspReportToURL().getLocalURIString() + "\""; // Initialized on first request and reset when allowed sources change. Don't reference this directly; always use diff --git a/assay/src/org/labkey/assay/AssayIntegrationTestCase.jsp b/assay/src/org/labkey/assay/AssayIntegrationTestCase.jsp index cb7e846a345..491d652a502 100644 --- a/assay/src/org/labkey/assay/AssayIntegrationTestCase.jsp +++ b/assay/src/org/labkey/assay/AssayIntegrationTestCase.jsp @@ -580,7 +580,7 @@ updated.put("ResultProp", 200); updated.put("RowId", resultRowId); errors = new BatchValidationException(); - Thread.sleep(5); // SQL Server timestamps aren't granular enough to guarantee different modified time + Thread.sleep(schema.getDbSchema().getSqlDialect().isSqlServer() ? 100 : 5); // SQL Server timestamps aren't granular enough to guarantee different modified time resultsQUS.updateRows(user, c, Collections.singletonList(updated), null, errors, null, null); // verify result created matches run's created in query table, but result modified now differs from run's created diff --git a/core/src/org/labkey/core/login/LoginController.java b/core/src/org/labkey/core/login/LoginController.java index c568c93240d..7185d60451d 100644 --- a/core/src/org/labkey/core/login/LoginController.java +++ b/core/src/org/labkey/core/login/LoginController.java @@ -60,6 +60,7 @@ import org.labkey.api.security.AuthenticationManager.AuthenticationStatus; import org.labkey.api.security.AuthenticationManager.LoginReturnProperties; import org.labkey.api.security.AuthenticationManager.PrimaryAuthenticationResult; +import org.labkey.api.security.AuthenticationManager.Reauth; import org.labkey.api.security.AuthenticationProvider; import org.labkey.api.security.AuthenticationProvider.SSOAuthenticationProvider; import org.labkey.api.security.CSRF; @@ -92,6 +93,7 @@ import org.labkey.api.settings.WriteableLookAndFeelProperties; import org.labkey.api.util.CSRFUtil; import org.labkey.api.util.ConfigurationException; +import org.labkey.api.util.GUID; import org.labkey.api.util.HelpTopic; import org.labkey.api.util.HtmlString; import org.labkey.api.util.MailHelper; @@ -139,6 +141,7 @@ import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_LIMIT_KEY; import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_PERIOD_KEY; import static org.labkey.api.security.AuthenticationManager.LOGIN_ATTEMPT_RESET_TIME_KEY; +import static org.labkey.api.security.AuthenticationManager.REAUTH_TOKEN_NAME; import static org.labkey.api.security.AuthenticationManager.SELF_REGISTRATION_KEY; import static org.labkey.api.security.AuthenticationManager.SELF_SERVICE_EMAIL_CHANGES_KEY; @@ -244,6 +247,13 @@ public ActionURL getLoginURL(Container c, @Nullable URLHelper returnUrl) return url; } + @Override + public ActionURL getForceReauthURL(Container c, @Nullable URLHelper returnUrl) + { + return getLoginURL(c, returnUrl) + .addParameter("forceReauth", true); + } + @Override public ActionURL getRegisterURL(Container c, @Nullable URLHelper returnUrl) { @@ -387,7 +397,7 @@ public class RegisterAction extends SimpleViewAction @Override public ModelAndView getView(RegisterForm form, BindException errors) { - ModelAndView redirectView = redirectIfLoggedIn(form); + ModelAndView redirectView = redirectIfLoggedIn(form, false); if (redirectView != null) return redirectView; if (!AuthenticationManager.isRegistrationEnabled()) @@ -581,19 +591,20 @@ public void setProvider(String provider) * @return a view that will redirect the user if they're already logged in, or null if they're not logged in already */ @Nullable - private ModelAndView redirectIfLoggedIn(AbstractLoginForm form) + private ModelAndView redirectIfLoggedIn(AbstractLoginForm form, boolean forceLogin) { - if (!getUser().isGuest()) + if (getUser().isGuest() || forceLogin) { - URLHelper returnUrl = form.getReturnUrlHelper(); + return null; + } - // Create LoginReturnProperties if we have a returnUrl or skipProfile param - LoginReturnProperties properties = null != returnUrl || form.getSkipProfile() - ? new LoginReturnProperties(returnUrl, form.getUrlhash(), form.getSkipProfile()) : null; + URLHelper returnUrl = form.getReturnUrlHelper(); - throw new ExternalRedirectException(AuthenticationManager.getAfterLoginURL(getContainer(), properties, getUser())); - } - return null; + // Create LoginReturnProperties if we have a returnUrl or skipProfile param + LoginReturnProperties properties = null != returnUrl || form.getSkipProfile() + ? new LoginReturnProperties(returnUrl, form.getUrlhash(), form.getSkipProfile()) : null; + + throw new ExternalRedirectException(AuthenticationManager.getAfterLoginURL(getContainer(), properties, getUser())); } @RequiresNoPermission @@ -609,7 +620,7 @@ public ModelAndView getView(LoginForm form, BindException errors) var canonicalUrl = PageFlowUtil.urlProvider(LoginUrls.class).getLoginURL(ContainerManager.getRoot(), null); getPageConfig().setCanonicalLink(canonicalUrl.getURIString()); - ModelAndView redirectView = redirectIfLoggedIn(form); + ModelAndView redirectView = redirectIfLoggedIn(form, form.isForceReauth()); if (redirectView != null) return redirectView; HttpServletRequest request = getViewContext().getRequest(); @@ -675,7 +686,10 @@ public Object execute(LoginForm form, BindException errors) if (success) { - AuthenticationResult authResult = AuthenticationManager.handleAuthentication(request, getContainer()); + // Don't touch the session in the re-auth case (e.g., CAS renew=true). The CAS spec is silent on + // expected behavior when no "ticket-signing ticket" (session, in our case) exists and a "renew" is + // requested, but this seems consistent with "ignore the current session" when renew is requested. + AuthenticationResult authResult = AuthenticationManager.handleAuthentication(request, getContainer(), !form.isForceReauth()); // getUser will return null if authentication is incomplete as is the case when secondary authentication is required User user = authResult.getUser(); URLHelper redirectUrl = authResult.getRedirectURL(); @@ -691,6 +705,13 @@ else if (form.getTermsOfUseType() == TermsOfUseType.SITE_WIDE) response.put("approvedTermsOfUse", true); } + if (form.isForceReauth()) + { + String reauthToken = GUID.makeHash(); + redirectUrl.addParameter(REAUTH_TOKEN_NAME, reauthToken); + request.getSession().setAttribute(REAUTH_TOKEN_NAME, new Reauth(reauthToken, user)); + } + // Use the full hostname in the URL if we have one, otherwise just go with a local URI String redirectString = redirectUrl.getHost() != null && redirectUrl.getScheme() != null ? redirectUrl.getURIString() : redirectUrl.toString(); @@ -1364,23 +1385,20 @@ public void setApprovedTermsOfUse(boolean approvedTermsOfUse) public static class LoginForm extends AgreeToTermsForm { - private boolean remember; private String email; private String password; private String provider; + private boolean forceReauth = false; - public void setProvider(String provider) - { - this.provider = provider; - } - public void setEmail(String email) + public String getProvider() { - this.email = email; + return this.provider; } - public String getProvider() + @SuppressWarnings({"UnusedDeclaration"}) + public void setProvider(String provider) { - return this.provider; + this.provider = provider; } public String getEmail() @@ -1388,6 +1406,12 @@ public String getEmail() return this.email; } + @SuppressWarnings({"UnusedDeclaration"}) + public void setEmail(String email) + { + this.email = email; + } + public String getPassword() { return password; @@ -1399,15 +1423,15 @@ public void setPassword(String password) this.password = password; } - public boolean isRemember() + public boolean isForceReauth() { - return this.remember; + return forceReauth; } - @SuppressWarnings({"UnusedDeclaration"}) - public void setRemember(boolean remember) + @SuppressWarnings("unused") + public void setForceReauth(boolean forceReauth) { - this.remember = remember; + this.forceReauth = forceReauth; } } diff --git a/core/webapp/login.js b/core/webapp/login.js index e4f4832fe51..e9c4bcc9199 100644 --- a/core/webapp/login.js +++ b/core/webapp/login.js @@ -37,14 +37,14 @@ url: LABKEY.ActionURL.buildURL('login', 'loginApi.api', this.containerPath), method: 'POST', params: { - remember: document.getElementById('remember').value, email: document.getElementById('email').value, password: document.getElementById('password').value, approvedTermsOfUse: document.getElementById('approvedTermsOfUse').checked, termsOfUseType: document.getElementById('termsOfUseType').value, returnUrl: returnUrlElement && returnUrlElement.value ? returnUrlElement.value : LABKEY.ActionURL.getParameter("returnUrl"), skipProfile: LABKEY.ActionURL.getParameter("skipProfile") || 0, - urlhash: document.getElementById('urlhash').value + urlhash: document.getElementById('urlhash').value, + forceReauth: LABKEY.ActionURL.getParameter("forceReauth") || false }, success: LABKEY.Utils.getCallbackWrapper(function(response) { setSubmitting(false, [{msg: ''}]);