diff --git a/api/src/org/labkey/api/action/SimpleErrorView.java b/api/src/org/labkey/api/action/SimpleErrorView.java index b7f58fcd742..b50c5cea200 100644 --- a/api/src/org/labkey/api/action/SimpleErrorView.java +++ b/api/src/org/labkey/api/action/SimpleErrorView.java @@ -23,8 +23,6 @@ /** * View that renders an error collection. - * User: adam - * Date: Sep 26, 2007 */ public class SimpleErrorView extends JspView { diff --git a/api/src/org/labkey/api/security/AuthenticationConfiguration.java b/api/src/org/labkey/api/security/AuthenticationConfiguration.java index 9cc866d7432..d308e98c9da 100644 --- a/api/src/org/labkey/api/security/AuthenticationConfiguration.java +++ b/api/src/org/labkey/api/security/AuthenticationConfiguration.java @@ -114,6 +114,7 @@ interface SSOAuthenticationConfiguration { LinkFactory getLinkFactory(); URLHelper getUrl(ViewContext ctx); + URLHelper getReauthUrl(ViewContext ctx); /** * Allows an SSO auth configuration to specify that it should be used automatically instead of showing the standard diff --git a/api/src/org/labkey/api/security/AuthenticationManager.java b/api/src/org/labkey/api/security/AuthenticationManager.java index c6051d89850..7515b5b31d7 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -86,6 +86,7 @@ import org.labkey.api.usageMetrics.UsageMetricsService; import org.labkey.api.util.DateUtil; import org.labkey.api.util.ExceptionUtil; +import org.labkey.api.util.GUID; import org.labkey.api.util.HeartBeat; import org.labkey.api.util.HtmlString; import org.labkey.api.util.HtmlStringBuilder; @@ -549,7 +550,12 @@ public static abstract class BaseSsoValidateAction
_userAttributeMap = Collections.emptyMap(); // A case-insensitive map of attribute names and values associated with the user private @NotNull Map _authenticationProperties = Collections.emptyMap(); private boolean _requireSecondary = true; // Require secondary authentication + private boolean _reauth = false; private @Nullable String _successDetails = null; // An optional string describing how successful authentication took place, which will // appear in the audit log. If null, the configuration's description will be used. @@ -447,6 +448,17 @@ public AuthenticationResponse setRequireSecondary(boolean requireSecondary) _requireSecondary = requireSecondary; return this; } + + public boolean isReauth() + { + return _reauth; + } + + public AuthenticationResponse setReauth(boolean reauth) + { + _reauth = reauth; + return this; + } } // FailureReasons are only reported to administrators (in the audit log and/or server log), NOT to users (and potential diff --git a/api/src/org/labkey/api/security/LoginUrls.java b/api/src/org/labkey/api/security/LoginUrls.java index 856ffb05afe..b1739d85432 100644 --- a/api/src/org/labkey/api/security/LoginUrls.java +++ b/api/src/org/labkey/api/security/LoginUrls.java @@ -35,10 +35,11 @@ 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 getForceReauthURL(Container c, boolean local, @Nullable URLHelper returnUrl); ActionURL getLogoutURL(Container c); ActionURL getLogoutURL(Container c, URLHelper returnUrl); ActionURL getStopImpersonatingURL(Container c, @Nullable URLHelper returnUrl); ActionURL getAgreeToTermsURL(Container c, URLHelper returnUrl); ActionURL getSSORedirectURL(SSOAuthenticationConfiguration configuration, URLHelper returnUrl, boolean skipProfile); + ActionURL getSSOReauthURL(SSOAuthenticationConfiguration configuration, URLHelper returnUrl); } diff --git a/core/src/org/labkey/core/login/LoginController.java b/core/src/org/labkey/core/login/LoginController.java index 5e1e40e98d5..3c63e9fcd2d 100644 --- a/core/src/org/labkey/core/login/LoginController.java +++ b/core/src/org/labkey/core/login/LoginController.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.json.JSONArray; +import org.json.JSONObject; import org.labkey.api.action.ApiResponse; import org.labkey.api.action.ApiSimpleResponse; import org.labkey.api.action.ApiUsageException; @@ -52,6 +53,7 @@ import org.labkey.api.security.ActionNames; import org.labkey.api.security.AdminConsoleAction; import org.labkey.api.security.AuthenticationConfiguration.LoginFormAuthenticationConfiguration; +import org.labkey.api.security.AuthenticationConfiguration.PrimaryAuthenticationConfiguration; import org.labkey.api.security.AuthenticationConfiguration.SSOAuthenticationConfiguration; import org.labkey.api.security.AuthenticationConfiguration.SecondaryAuthenticationConfiguration; import org.labkey.api.security.AuthenticationConfigurationCache; @@ -60,7 +62,6 @@ 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; @@ -72,6 +73,7 @@ import org.labkey.api.security.MutableSecurityPolicy; import org.labkey.api.security.PasswordExpiration; import org.labkey.api.security.PasswordRule; +import org.labkey.api.security.RequiresLogin; import org.labkey.api.security.RequiresNoPermission; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.SecurityManager; @@ -93,7 +95,6 @@ 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; @@ -115,6 +116,7 @@ import org.labkey.api.view.RedirectException; import org.labkey.api.view.UnsafeExternalRedirectException; import org.labkey.api.view.VBox; +import org.labkey.api.view.ViewContext; import org.labkey.api.view.WebPartView; import org.labkey.api.view.template.PageConfig; import org.labkey.api.wiki.WikiRendererType; @@ -141,7 +143,6 @@ 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; @@ -248,10 +249,16 @@ public ActionURL getLoginURL(Container c, @Nullable URLHelper returnUrl) } @Override - public ActionURL getForceReauthURL(Container c, @Nullable URLHelper returnUrl) + public ActionURL getForceReauthURL(Container c, boolean local, @Nullable URLHelper returnUrl) { - return getLoginURL(c, returnUrl) + ActionURL url = getLoginURL(c, returnUrl) .addParameter("forceReauth", true); + + // Customizes re-auth behavior for the local login page case (vs. CAS IdP case) + if (local) + url.addParameter("local", true); + + return url; } @Override @@ -301,12 +308,24 @@ public ActionURL getAgreeToTermsURL(Container c, URLHelper returnUrl) @Override public ActionURL getSSORedirectURL(SSOAuthenticationConfiguration configuration, URLHelper returnUrl, boolean skipProfile) { - ActionURL url = new ActionURL(SsoRedirectAction.class, ContainerManager.getRoot()); - url.addParameter("configuration", configuration.getRowId()); + ActionURL url = getRedirectURL(SsoRedirectAction.class, configuration, returnUrl); if (skipProfile) { url.addParameter("skipProfile", 1); } + return url; + } + + @Override + public ActionURL getSSOReauthURL(SSOAuthenticationConfiguration configuration, URLHelper returnUrl) + { + return getRedirectURL(SsoReauthAction.class, configuration, returnUrl); + } + + private ActionURL getRedirectURL(Class redirectActionClass, SSOAuthenticationConfiguration configuration, @Nullable URLHelper returnUrl) + { + ActionURL url = new ActionURL(redirectActionClass, ContainerManager.getRoot()); + url.addParameter("configuration", configuration.getRowId()); if (null != returnUrl) { String fragment = returnUrl.getFragment(); @@ -650,7 +669,7 @@ public class LoginApiAction extends MutatingApiAction @Override public Object execute(LoginForm form, BindException errors) { - HttpServletRequest request = getViewContext().getRequest(); + HttpServletRequest request = getViewContext().getRequestOrThrow(); // Store passed in returnUrl and skipProfile param at the start of the login so we can redirect to it after // any password resets, secondary logins, profile updates, etc. have finished @@ -707,9 +726,7 @@ else if (form.getTermsOfUseType() == TermsOfUseType.SITE_WIDE) if (form.isForceReauth()) { - String reauthToken = GUID.makeHash(); - redirectUrl.addParameter(REAUTH_TOKEN_NAME, reauthToken); - request.getSession().setAttribute(REAUTH_TOKEN_NAME, new Reauth(reauthToken, user)); + AuthenticationManager.setReauthUser(user, form.isLocal() ? getUser() : null, request, null, redirectUrl); } // Use the full hostname in the URL if we have one, otherwise just go with a local URI @@ -1388,7 +1405,8 @@ public static class LoginForm extends AgreeToTermsForm private String email; private String password; private String provider; - private boolean forceReauth = false; + private boolean forceReauth = false; // If true, require valid credentials even if logged in + private boolean local = false; // If true, require on re-auth that current session user matches re-auth user public String getProvider() { @@ -1433,6 +1451,17 @@ public void setForceReauth(boolean forceReauth) { this.forceReauth = forceReauth; } + + public boolean isLocal() + { + return local; + } + + @SuppressWarnings("unused") + public void setLocal(boolean local) + { + this.local = local; + } } @RequiresNoPermission @@ -1539,20 +1568,8 @@ public Object execute(ReturnUrlForm form, BindException errors) public static class SsoRedirectForm extends AbstractLoginForm { - private String _provider; private int _configuration; - public String getProvider() - { - return _provider; - } - - @SuppressWarnings("unused") - public void setProvider(String provider) - { - _provider = provider; - } - public int getConfiguration() { return _configuration; @@ -1565,18 +1582,13 @@ public void setConfiguration(int configuration) } } - @RequiresNoPermission - @AllowedDuringUpgrade - // Always invoked in the root, so no need to ignore locked projects - public static class SsoRedirectAction extends SimpleViewAction + private static abstract class BaseSsoRedirectAction extends SimpleViewAction { + protected abstract URLHelper getUrl(SSOAuthenticationConfiguration configuration, ViewContext context); + @Override public ModelAndView getView(SsoRedirectForm form, BindException errors) { - // If logged in then redirect immediately - if (!getUser().isGuest()) - return HttpView.redirect(form.getReturnActionURL(AppProps.getInstance().getHomePageActionURL())); - // If we have a returnUrl or skipProfile param then create and stash LoginReturnProperties URLHelper returnUrl = form.getReturnUrlHelper(); if (null != returnUrl || form.getSkipProfile()) @@ -1592,7 +1604,7 @@ public ModelAndView getView(SsoRedirectForm form, BindException errors) if (null == configuration) throw new NotFoundException("Authentication configuration is not valid"); - url = configuration.getUrl(getViewContext()); + url = getUrl(configuration, getViewContext()); // It's safe to bypass checking the external redirect allow list in this case because we are redirecting to // the administrator-provided URL from the SSO authentication configuration. @@ -1605,6 +1617,62 @@ public final void addNavTrail(NavTree root) } } + @RequiresNoPermission + @AllowedDuringUpgrade + // Always invoked in the root, so no need to ignore locked projects + public static class SsoRedirectAction extends BaseSsoRedirectAction + { + @Override + public ModelAndView getView(SsoRedirectForm form, BindException errors) + { + // If logged in then redirect immediately + if (!getUser().isGuest()) + return HttpView.redirect(form.getReturnActionURL(AppProps.getInstance().getHomePageActionURL())); + + return super.getView(form, errors); + } + + @Override + protected URLHelper getUrl(SSOAuthenticationConfiguration configuration, ViewContext context) + { + return configuration.getUrl(context); + } + } + + // Very similar to SsoRedirectAction, but needs different annotations, so we have two separate classes + @RequiresLogin + public static class SsoReauthAction extends BaseSsoRedirectAction + { + @Override + protected URLHelper getUrl(SSOAuthenticationConfiguration configuration, ViewContext context) + { + return configuration.getReauthUrl(context); + } + } + + @SuppressWarnings("unused") // Called from client code + @RequiresLogin + public static class GetAuthenticationConfigurationAction extends ReadOnlyApiAction + { + @Override + public Object execute(ReturnUrlForm form, BindException errors) + { + PrimaryAuthenticationConfiguration configuration = AuthenticationManager.getConfiguration(getViewContext().getSession()); + if (configuration == null) + { + throw new RuntimeException("No configuration found"); + } + JSONObject resp = new JSONObject(); + resp.put("description", configuration.getDescription()); + LoginUrls urls = urlProvider(LoginUrls.class); + ActionURL reauthUrl = configuration instanceof SSOAuthenticationConfiguration sso ? + urls.getSSOReauthURL(sso, form.getReturnActionURL()) : + urls.getForceReauthURL(getContainer(), true, form.getReturnActionURL()); + resp.put("reauthUrl", reauthUrl.getLocalURIString()); + return success(resp); + } + } + public static final String PASSWORD1_TEXT_FIELD_NAME = "password"; public static final String PASSWORD2_TEXT_FIELD_NAME = "password2"; diff --git a/core/webapp/login.js b/core/webapp/login.js index e2534ede023..a40073b8fb9 100644 --- a/core/webapp/login.js +++ b/core/webapp/login.js @@ -44,7 +44,8 @@ returnUrl: returnUrlElement && returnUrlElement.value ? returnUrlElement.value : LABKEY.ActionURL.getParameter("returnUrl"), skipProfile: LABKEY.ActionURL.getParameter("skipProfile") || 0, urlhash: document.getElementById('urlhash').value, - forceReauth: LABKEY.ActionURL.getParameter("forceReauth") || false + forceReauth: LABKEY.ActionURL.getParameter("forceReauth") || false, + local: LABKEY.ActionURL.getParameter("local") || false }, success: LABKEY.Utils.getCallbackWrapper(function(response) { setSubmitting(false, [{msg: ''}]); diff --git a/devtools/src/org/labkey/devtools/TestController.java b/devtools/src/org/labkey/devtools/TestController.java index 1240c01fb0e..6c49f6572c5 100644 --- a/devtools/src/org/labkey/devtools/TestController.java +++ b/devtools/src/org/labkey/devtools/TestController.java @@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Test; import org.labkey.api.action.ApiResponse; @@ -40,12 +41,14 @@ import org.labkey.api.mcp.AbstractAgentAction; import org.labkey.api.mcp.McpService; import org.labkey.api.security.AdminConsoleAction; +import org.labkey.api.security.AuthenticationManager; import org.labkey.api.security.CSRF; import org.labkey.api.security.MethodsAllowed; import org.labkey.api.security.RequiresLogin; import org.labkey.api.security.RequiresNoPermission; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.RequiresSiteAdmin; +import org.labkey.api.security.User; import org.labkey.api.security.permissions.AdminOperationsPermission; import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.security.permissions.DeletePermission; @@ -1492,4 +1495,41 @@ public void testJacksonInputLimitExceeded() throws Exception } } + public record ReauthForm(@Nullable String reauthToken, @Nullable String errorMessage){} + + @RequiresLogin + public class TestReauthAction extends FormViewAction + { + @Override + public void validateCommand(ReauthForm form, Errors errors) + { + } + + @Override + public ModelAndView getView(ReauthForm form, boolean reshow, BindException errors) + { + getPageConfig().setTemplate(PageConfig.Template.Dialog); + return new JspView<>("/org/labkey/devtools/view/testReauth.jsp", form, errors); + } + + @Override + public boolean handlePost(ReauthForm form, BindException errors) + { + User reauthUser = AuthenticationManager.getAndClearReauthUser(getViewContext().getRequestOrThrow(), form.reauthToken(), getUser()); + if (reauthUser == null) + throw new NotFoundException("Reauthentication validation failed!"); + return true; + } + + @Override + public URLHelper getSuccessURL(ReauthForm form) + { + return actionURL(BeginAction.class); + } + + @Override + public void addNavTrail(NavTree root) + { + } + } } diff --git a/devtools/src/org/labkey/devtools/authentication/TestSsoConfiguration.java b/devtools/src/org/labkey/devtools/authentication/TestSsoConfiguration.java index 34da73011d4..fb0314f1def 100644 --- a/devtools/src/org/labkey/devtools/authentication/TestSsoConfiguration.java +++ b/devtools/src/org/labkey/devtools/authentication/TestSsoConfiguration.java @@ -15,6 +15,7 @@ */ package org.labkey.devtools.authentication; +import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.ContainerManager; @@ -45,7 +46,7 @@ protected TestSsoConfiguration(TestSsoProvider provider, Map sta } @Override - public URLHelper getUrl(ViewContext ctx) + public ActionURL getUrl(ViewContext ctx) { ActionURL url = new ActionURL(TestSsoController.TestSsoAction.class, ContainerManager.getRoot()); url.addParameter("configuration", getRowId()); @@ -53,6 +54,12 @@ public URLHelper getUrl(ViewContext ctx) return url; } + @Override + public ActionURL getReauthUrl(ViewContext ctx) + { + return getUrl(ctx).addParameter("reauth", true); + } + @Override public LinkFactory getLinkFactory() { diff --git a/devtools/src/org/labkey/devtools/authentication/TestSsoController.java b/devtools/src/org/labkey/devtools/authentication/TestSsoController.java index 06290a5abcd..1017fc879a5 100644 --- a/devtools/src/org/labkey/devtools/authentication/TestSsoController.java +++ b/devtools/src/org/labkey/devtools/authentication/TestSsoController.java @@ -50,10 +50,10 @@ public TestSsoController() @RequiresNoPermission @AllowedDuringUpgrade - public static class TestSsoAction extends SimpleViewAction + public static class TestSsoAction extends SimpleViewAction { @Override - public ModelAndView getView(AuthenticationConfigurationForm form, BindException errors) + public ModelAndView getView(TestSsoForm form, BindException errors) { getPageConfig().setTemplate(PageConfig.Template.Dialog); return new JspView<>("/org/labkey/devtools/authentication/testSso.jsp", form, errors); @@ -68,6 +68,7 @@ public void addNavTrail(NavTree root) public static class TestSsoForm extends AuthenticationConfigurationForm { private String _email; + private boolean _reauth; public String getEmail() { @@ -79,6 +80,17 @@ public void setEmail(String email) { _email = email; } + + public boolean isReauth() + { + return _reauth; + } + + @SuppressWarnings("unused") + public void setReauth(boolean reauth) + { + _reauth = reauth; + } } @AllowedDuringUpgrade @@ -94,7 +106,7 @@ public static class ValidateAction extends BaseSsoValidateAction if (null == configuration) throw new NotFoundException("Invalid TestSso configuration"); - return AuthenticationResponse.success(configuration, new ValidEmail(form.getEmail())); + return AuthenticationResponse.success(configuration, new ValidEmail(form.getEmail())).setReauth(form.isReauth()); } } diff --git a/devtools/src/org/labkey/devtools/authentication/testSso.jsp b/devtools/src/org/labkey/devtools/authentication/testSso.jsp index 246ced97d51..68020d24668 100644 --- a/devtools/src/org/labkey/devtools/authentication/testSso.jsp +++ b/devtools/src/org/labkey/devtools/authentication/testSso.jsp @@ -15,21 +15,22 @@ * limitations under the License. */ %> -<%@ page import="org.labkey.api.security.AuthenticationManager.AuthenticationConfigurationForm" %> <%@ page import="org.labkey.api.view.HttpView" %> <%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.devtools.authentication.TestSsoController.TestSsoForm" %> <%@ page import="org.labkey.devtools.authentication.TestSsoController.ValidateAction" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <% - JspView me = HttpView.currentView(); - AuthenticationConfigurationForm form = me.getModelBean(); + JspView me = HttpView.currentView(); + TestSsoForm form = me.getModelBean(); %> + - <%= button("Authenticate").submit(true) %> + <%= button(form.isReauth() ? "Reauthenticate" : "Authenticate").submit(true) %> \ No newline at end of file diff --git a/devtools/src/org/labkey/devtools/view/testReauth.jsp b/devtools/src/org/labkey/devtools/view/testReauth.jsp new file mode 100644 index 00000000000..7262bcdbce7 --- /dev/null +++ b/devtools/src/org/labkey/devtools/view/testReauth.jsp @@ -0,0 +1,81 @@ +<% +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.devtools.TestController.ReauthForm" %> +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + JspView me = HttpView.currentView(); + ReauthForm form = me.getModelBean(); +%> + + +You authenticated with:
+ +<% + if (form.reauthToken() != null) + { +%> +Looks like you successfully re-authenticated and received token: <%=h(form.reauthToken())%>
+ + + + + +<% + } + else + { + if (form.errorMessage() != null) + { +%> +Looks like your reauthentication failed: <%=h(form.errorMessage())%>. Try again? +<% + } + else + { +%> +You need to re-authenticate. +<% + } +%> + Click here +<% + } +%>