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
80 changes: 74 additions & 6 deletions api/src/org/labkey/api/security/AuthenticationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,10 @@ private ModelAndView getReauthView(AuthenticationResponse response, BindExceptio

AuthenticationManager.setReauthUser(reauthUser, getUser(), getViewContext().getRequestOrThrow(), errorMessage, url);

// A token on the URL means setReauthUser() accepted the reauthentication.
if (null != reauthUser && null != url.getParameter(REAUTH_TOKEN_NAME))
AuthenticationManager.auditReauthSuccess(reauthUser, response);

throw new RedirectException(url);
}

Expand Down Expand Up @@ -1091,14 +1095,24 @@ public static void addAuditEvent(@NotNull User user, HttpServletRequest request,


public static @NotNull PrimaryAuthenticationResult authenticate(HttpServletRequest request, String id, String password, URLHelper returnUrl, boolean logFailures) throws InvalidEmailException
{
return authenticate(request, id, password, returnUrl, logFailures, false);
}


/**
* @param reauth True when reauthenticating an already signed-in user rather than logging one in. See
* {@link #finalizePrimaryAuthentication(HttpServletRequest, AuthenticationResponse, boolean)}.
*/
public static @NotNull PrimaryAuthenticationResult authenticate(HttpServletRequest request, String id, String password, URLHelper returnUrl, boolean logFailures, boolean reauth) throws InvalidEmailException
{
PrimaryAuthenticationResult result = null;
try
{
result = _beforeAuthenticate(request, id, password);
if (null != result)
return result;
result = _authenticate(request, id, password, returnUrl, logFailures);
result = _authenticate(request, id, password, returnUrl, logFailures, reauth);
return result;
}
finally
Expand All @@ -1108,7 +1122,7 @@ public static void addAuditEvent(@NotNull User user, HttpServletRequest request,
}


private static @NotNull PrimaryAuthenticationResult _authenticate(HttpServletRequest request, final String id, String password, URLHelper returnUrl, boolean logFailures) throws InvalidEmailException
private static @NotNull PrimaryAuthenticationResult _authenticate(HttpServletRequest request, final String id, String password, URLHelper returnUrl, boolean logFailures, boolean reauth) throws InvalidEmailException
{
if (areNotBlank(id, password))
{
Expand All @@ -1132,7 +1146,7 @@ public static void addAuditEvent(@NotNull User user, HttpServletRequest request,

if (authResponse.isAuthenticated())
{
return finalizePrimaryAuthentication(request, authResponse);
return finalizePrimaryAuthentication(request, authResponse, reauth);
}
else
{
Expand Down Expand Up @@ -1229,6 +1243,18 @@ else if (null != emailAddress)

@NotNull
public static PrimaryAuthenticationResult finalizePrimaryAuthentication(HttpServletRequest request, AuthenticationResponse response)
{
return finalizePrimaryAuthentication(request, response, false);
}

/**
* @param reauth True when reauthenticating an already signed-in user rather than logging one in. Suppresses the
* "logged in" audit event: no session is created and the user was already signed in, so recording a
* login misstates what happened. Callers passing true are responsible for recording the
* reauthentication via {@link #auditReauthSuccess(User, AuthenticationResponse)}.
*/
@NotNull
public static PrimaryAuthenticationResult finalizePrimaryAuthentication(HttpServletRequest request, AuthenticationResponse response, boolean reauth)
{
User user = response.getUser();
final String emailAddress;
Expand Down Expand Up @@ -1288,7 +1314,8 @@ public static PrimaryAuthenticationResult finalizePrimaryAuthentication(HttpServ
return new PrimaryAuthenticationResult(AuthenticationStatus.InactiveUser);
}

addAuditEvent(user, request, emailAddress + " " + UserManager.UserAuditEvent.LOGGED_IN + " successfully via " + response.getSuccessDetails() + ".");
if (!reauth)
addAuditEvent(user, request, emailAddress + " " + UserManager.UserAuditEvent.LOGGED_IN + " successfully via " + response.getSuccessDetails() + ".");

return new PrimaryAuthenticationResult(user, response);
}
Expand Down Expand Up @@ -1714,6 +1741,11 @@ public URLHelper getRedirectURL()
session.removeAttribute(getReauthFlowSessionKey());
URLHelper url = getAfterReauthURL(c, getLoginReturnProperties(request), primaryAuthUser);
setReauthUser(primaryAuthUser, reauthFlow.local() ? SecurityManager.getSessionUser(request) : null, request, null, url);

// A token on the URL means setReauthUser() accepted the reauthentication.
if (null != url.getParameter(REAUTH_TOKEN_NAME))
auditReauthSuccess(primaryAuthUser, primaryAuthResult.getResponse());

return new AuthenticationResult(primaryAuthUser, url);
}

Expand Down Expand Up @@ -1884,11 +1916,33 @@ public boolean isExpired()
* @param errorMessage Pre-existing error message to add to the URL
* @param redirectUrl URL to which the token (on success) or error message (on failure) gets added
*/
public static void setReauthUser(User reauthUser, @Nullable User sessionUser, HttpServletRequest request, @Nullable String errorMessage, URLHelper redirectUrl)
public static void setReauthUser(@Nullable User reauthUser, @Nullable User sessionUser, HttpServletRequest request, @Nullable String errorMessage, URLHelper redirectUrl)
{
if (errorMessage == null && sessionUser != null && !sessionUser.equals(reauthUser))
{
errorMessage = "Reauthentication failed: wrong user reauthenticated";
// One condition in code, but three different problems in practice -- sign in as the right user, fix the
// IdP's claim mapping, or fix the session cookie -- so each gets a message that says which one it is.
if (sessionUser.isGuest())
{
// The SSO validate actions are @RequiresNoPermission, so getUser() returns guest whenever the request
// carries no signed-in session -- typically because the session cookie didn't accompany the IdP's
// cross-site POST to the validate action, or because the session timed out mid-flow.
errorMessage = "Reauthentication failed: this browser is no longer signed in; please sign in again";
_log.warn("Reauthentication failed for \"{}\": the request carried no signed-in session. Check that the session cookie accompanies the identity provider's response to the validate action -- a JSESSIONID with no explicit SameSite value is withheld from that cross-site POST once it is more than a couple of minutes old.", null != reauthUser ? reauthUser.getEmail() : "an unrecognized identity");
}
else if (null == reauthUser)
// Narrow, but sign-in and reauthentication resolve users differently: finalizePrimaryAuthentication()
// can auto-create an account, and reauthentication never does. Reaching here means the asserted
// identity has no account by the time reauth runs -- deleted or renamed mid-session, or the IdP
// asserting a different identifier than it did at sign-in.
errorMessage = "Reauthentication failed: the reauthenticated identity does not match a LabKey user account";
else
{
errorMessage = "Reauthentication failed: wrong user reauthenticated";
// The only place that knows both identities. The audit log records neither, since no reauthentication
// completed, so without this the pairing can't be reconstructed afterward.
_log.warn("Reauthentication failed for \"{}\": \"{}\" reauthenticated instead.", sessionUser.getEmail(), reauthUser.getEmail());
}
}

if (errorMessage != null)
Expand All @@ -1905,6 +1959,20 @@ public static void setReauthUser(User reauthUser, @Nullable User sessionUser, Ht
}
}

/**
* Records that a reauthentication happened. SSO reauth never reaches finalizePrimaryAuthentication(), so it left
* no server-side record at all, and local and signing reauth recorded themselves as logins. Mirrors the "logged
* in" event's phrasing so the two read alike in the audit log.
*/
public static void auditReauthSuccess(@NotNull User reauthUser, @NotNull AuthenticationResponse response)
{
// Calls UserManager.addAuditEvent() directly rather than this class's addAuditEvent(), which drops a message
// identical to the previous one from the same user and address. Signing several records in a row produces
// exactly those identical messages, and dropping them would leave real reauthentications unrecorded.
UserManager.addAuditEvent(reauthUser, ContainerManager.getRoot(), reauthUser,
reauthUser.getEmail() + " " + UserManager.UserAuditEvent.REAUTHENTICATED + " successfully via " + response.getSuccessDetails() + ".");
}

// Separate method to allow unit testing
private static void addToken(HttpServletRequest request, User reauthUser, String reauthToken, Instant expiration)
{
Expand Down
1 change: 1 addition & 0 deletions api/src/org/labkey/api/security/UserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@ public static class UserAuditEvent extends AuditTypeEvent
public static final String LOGGED_IN = "logged in";
public static final String LOGGED_OUT = "logged out";
public static final String API_KEY = "an API key";
public static final String REAUTHENTICATED = "reauthenticated";

int _user;

Expand Down
2 changes: 1 addition & 1 deletion core/src/org/labkey/core/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ private static boolean authenticate(LoginForm form, BindException errors, HttpSe
{
// Attempt authentication with all active form providers
String formEmail = form.getEmail();
PrimaryAuthenticationResult result = AuthenticationManager.authenticate(request, formEmail, form.getPassword(), form.getReturnUrlHelper(), true);
PrimaryAuthenticationResult result = AuthenticationManager.authenticate(request, formEmail, form.getPassword(), form.getReturnUrlHelper(), true, form.isForceReauth());
AuthenticationStatus status = result.getStatus();

if (Success == status)
Expand Down