Skip to content
Open
Show file tree
Hide file tree
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
17 changes: 9 additions & 8 deletions api/src/org/labkey/api/security/AuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)
{
Expand Down
13 changes: 8 additions & 5 deletions api/src/org/labkey/api/security/AuthenticatedRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
{
Expand Down
48 changes: 42 additions & 6 deletions api/src/org/labkey/api/security/AuthenticationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<HttpServletRequest>
Expand Down
1 change: 1 addition & 0 deletions api/src/org/labkey/api/security/LoginUrls.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions api/src/org/labkey/api/view/RedirectException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
5 changes: 2 additions & 3 deletions api/src/org/labkey/api/view/UnauthorizedException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion assay/src/org/labkey/assay/AssayIntegrationTestCase.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 50 additions & 26 deletions core/src/org/labkey/core/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -387,7 +397,7 @@ public class RegisterAction extends SimpleViewAction<RegisterForm>
@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())
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -1364,30 +1385,33 @@ 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()
{
return this.email;
}

@SuppressWarnings({"UnusedDeclaration"})
public void setEmail(String email)
{
this.email = email;
}

public String getPassword()
{
return password;
Expand All @@ -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;
}
}

Expand Down
4 changes: 2 additions & 2 deletions core/webapp/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''}]);
Expand Down