85276: Store and retrieve the Authentication method in the JWT

This commit is contained in:
Yana De Pauw
2021-12-01 15:33:13 +01:00
parent dcfa74a2c3
commit 16e704a285
14 changed files with 200 additions and 4 deletions

View File

@@ -216,4 +216,12 @@ public interface AuthenticationMethod {
* @return The authentication method name
*/
public String getName();
/**
* Get whether the authentication method is being used.
* @param context The DSpace context
* @param request The current request
* @return whether the authentication method is being used.
*/
public boolean isUsed(Context context, HttpServletRequest request);
}

View File

@@ -193,4 +193,17 @@ public class AuthenticationServiceImpl implements AuthenticationService {
public Iterator<AuthenticationMethod> authenticationMethodIterator() {
return getAuthenticationMethodStack().iterator();
}
public String getAuthenticationMethod(final Context context, final HttpServletRequest request) {
final Iterator<AuthenticationMethod> authenticationMethodIterator = authenticationMethodIterator();
while (authenticationMethodIterator.hasNext()) {
final AuthenticationMethod authenticationMethod = authenticationMethodIterator.next();
if (authenticationMethod.isUsed(context, request)) {
return authenticationMethod.getName();
}
}
return null;
}
}

View File

@@ -273,4 +273,9 @@ public class IPAuthentication implements AuthenticationMethod {
public String getName() {
return "ip";
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
return false;
}
}

View File

@@ -83,6 +83,9 @@ public class LDAPAuthentication
protected ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
private static final String LDAP_AUTHENTICATED = "ldap.authenticated";
/**
* Let a real auth method return true if it wants.
*
@@ -261,6 +264,7 @@ public class LDAPAuthentication
if (ldap.ldapAuthenticate(dn, password, context)) {
context.setCurrentUser(eperson);
request.getSession().setAttribute(LDAP_AUTHENTICATED, true);
// assign user to groups based on ldap dn
assignGroups(dn, ldap.ldapGroup, context);
@@ -311,6 +315,8 @@ public class LDAPAuthentication
context.dispatchEvents();
context.restoreAuthSystemState();
context.setCurrentUser(eperson);
request.getSession().setAttribute(LDAP_AUTHENTICATED, true);
// assign user to groups based on ldap dn
assignGroups(dn, ldap.ldapGroup, context);
@@ -341,6 +347,8 @@ public class LDAPAuthentication
ePersonService.update(context, eperson);
context.dispatchEvents();
context.setCurrentUser(eperson);
request.getSession().setAttribute(LDAP_AUTHENTICATED, true);
// assign user to groups based on ldap dn
assignGroups(dn, ldap.ldapGroup, context);
@@ -734,4 +742,14 @@ public class LDAPAuthentication
}
}
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
if (request != null &&
context.getCurrentUser() != null &&
request.getSession().getAttribute(LDAP_AUTHENTICATED) != null) {
return true;
}
return false;
}
}

View File

@@ -51,6 +51,9 @@ public class PasswordAuthentication
*/
private static final Logger log = LogManager.getLogger();
private static final String PASSWORD_AUTHENTICATED = "password.authenticated";
/**
* Look to see if this email address is allowed to register.
@@ -216,6 +219,7 @@ public class PasswordAuthentication
.checkPassword(context, eperson, password)) {
// login is ok if password matches:
context.setCurrentUser(eperson);
request.getSession().setAttribute(PASSWORD_AUTHENTICATED, true);
log.info(LogHelper.getHeader(context, "authenticate", "type=PasswordAuthentication"));
return SUCCESS;
} else {
@@ -247,4 +251,15 @@ public class PasswordAuthentication
public String getName() {
return "password";
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
if (request != null &&
context.getCurrentUser() != null &&
request.getSession().getAttribute(PASSWORD_AUTHENTICATED) != null) {
return true;
}
return false;
}
}

View File

@@ -1283,5 +1283,14 @@ public class ShibAuthentication implements AuthenticationMethod {
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
if (request != null &&
context.getCurrentUser() != null &&
request.getSession().getAttribute("shib.authenticated") != null) {
return true;
}
return false;
}
}

View File

@@ -128,6 +128,8 @@ public class X509Authentication implements AuthenticationMethod {
protected ConfigurationService configurationService =
DSpaceServicesFactory.getInstance().getConfigurationService();
private static final String X509_AUTHENTICATED = "x509.authenticated";
/**
* Initialization: Set caPublicKey and/or keystore. This loads the
@@ -544,6 +546,7 @@ public class X509Authentication implements AuthenticationMethod {
context.dispatchEvents();
context.restoreAuthSystemState();
context.setCurrentUser(eperson);
request.getSession().setAttribute(X509_AUTHENTICATED, true);
setSpecialGroupsFlag(request, email);
return SUCCESS;
} else {
@@ -563,6 +566,7 @@ public class X509Authentication implements AuthenticationMethod {
log.info(LogHelper.getHeader(context, "login",
"type=x509certificate"));
context.setCurrentUser(eperson);
request.getSession().setAttribute(X509_AUTHENTICATED, true);
setSpecialGroupsFlag(request, email);
return SUCCESS;
}
@@ -594,4 +598,14 @@ public class X509Authentication implements AuthenticationMethod {
public String getName() {
return "x509";
}
@Override
public boolean isUsed(final Context context, final HttpServletRequest request) {
if (request != null &&
context.getCurrentUser() != null &&
request.getSession().getAttribute(X509_AUTHENTICATED) != null) {
return true;
}
return false;
}
}

View File

@@ -168,4 +168,13 @@ public interface AuthenticationService {
*/
public Iterator<AuthenticationMethod> authenticationMethodIterator();
/**
* Retrieves the currently used authentication method name based on the context and the request
*
* @param context A valid DSpace context.
* @param request The request that started this operation, or null if not applicable.
* @return the currently used authentication method name
*/
public String getAuthenticationMethod(Context context, HttpServletRequest request);
}

View File

@@ -98,6 +98,11 @@ public class Context implements AutoCloseable {
*/
private List<UUID> specialGroupsPreviousState;
/**
* The currently used authentication method
*/
private String authenticationMethod;
/**
* Content events
*/
@@ -890,4 +895,11 @@ public class Context implements AutoCloseable {
currentUser = reloadEntity(currentUser);
}
public String getAuthenticationMethod() {
return authenticationMethod;
}
public void setAuthenticationMethod(final String authenticationMethod) {
this.authenticationMethod = authenticationMethod;
}
}

View File

@@ -118,6 +118,7 @@ public class AuthenticationRestController implements InitializingBean {
response.setHeader("WWW-Authenticate", authenticateHeaderValue);
}
authenticationStatusRest.setAuthenticationMethod(context.getAuthenticationMethod());
authenticationStatusRest.setProjection(projection);
AuthenticationStatusResource authenticationStatusResource = converter.toResource(authenticationStatusRest);

View File

@@ -16,6 +16,7 @@ import org.dspace.app.rest.RestResourceController;
public class AuthenticationStatusRest extends BaseObjectRest<Integer> {
private boolean okay;
private boolean authenticated;
private String authenticationMethod;
public static final String NAME = "status";
public static final String CATEGORY = RestAddressableModel.AUTHENTICATION;
@@ -81,4 +82,12 @@ public class AuthenticationStatusRest extends BaseObjectRest<Integer> {
public void setOkay(boolean okay) {
this.okay = okay;
}
public String getAuthenticationMethod() {
return authenticationMethod;
}
public void setAuthenticationMethod(final String authenticationMethod) {
this.authenticationMethod = authenticationMethod;
}
}

View File

@@ -0,0 +1,54 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security.jwt;
import java.sql.SQLException;
import java.text.ParseException;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Provides a claim for a JSON Web Token, this claim is responsible for adding the authentication method to it
*/
@Component
public class AuthenticationMethodClaimProvider implements JWTClaimProvider {
public static final String AUTHENTICATION_METHOD = "authenticationMethod";
private static final Logger log = LoggerFactory.getLogger(AuthenticationMethodClaimProvider.class);
@Autowired
private AuthenticationService authenticationService;
public String getKey() {
return AUTHENTICATION_METHOD;
}
public Object getValue(final Context context, final HttpServletRequest request) {
if (context.getAuthenticationMethod() != null) {
return context.getAuthenticationMethod();
}
return authenticationService.getAuthenticationMethod(context, request);
}
public void parseClaim(final Context context, final HttpServletRequest request, final JWTClaimsSet jwtClaimsSet)
throws SQLException {
try {
context.setAuthenticationMethod(jwtClaimsSet.getStringClaim(AUTHENTICATION_METHOD));
} catch (ParseException e) {
log.error(e.getMessage(), e);
}
}
}

View File

@@ -107,6 +107,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("password")))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
@@ -136,6 +137,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("password")))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
@@ -159,6 +161,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.authenticationMethod").doesNotExist())
.andExpect(jsonPath("$.type", is("status")))
.andExpect(header().string("WWW-Authenticate",
"password realm=\"DSpace REST API\""));
@@ -213,6 +216,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")))
// Verify that the CSRF token has NOT been changed... status checks won't change the token
// (only login/logout will)
@@ -244,6 +248,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")));
//Logout, invalidating the token
@@ -260,12 +265,18 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
// Verify /api/authn/shibboleth endpoint does not work
// NOTE: this is the same call as in testStatusShibAuthenticatedWithCookie())
getClient().perform(get("/api/authn/shibboleth")
String token = getClient().perform(get("/api/authn/shibboleth")
.header("Referer", "https://myshib.example.com")
.param("redirectUrl", uiURL)
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andReturn().getResponse().getHeader("Authorization");
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.authenticationMethod").doesNotExist());
}
// NOTE: This test is similar to testStatusShibAuthenticatedWithCookie(), but proves the same process works
@@ -453,6 +464,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.authenticationMethod").doesNotExist())
.andExpect(jsonPath("$.type", is("status")));
}
@@ -515,6 +527,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("password")))
.andExpect(jsonPath("$.type", is("status")));
// Logout, invalidating token
@@ -858,6 +871,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson",
@@ -901,6 +915,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson",
@@ -921,6 +936,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson",
@@ -954,6 +970,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("password")))
.andExpect(jsonPath("$.type", is("status")));
//Logout
@@ -965,6 +982,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.authenticationMethod").doesNotExist())
.andExpect(jsonPath("$.type", is("status")));
//Simulate that a shibboleth authentication has happened
@@ -979,6 +997,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")))
.andExpect(jsonPath("$.type", is("status")));
//Logout
@@ -990,6 +1009,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.authenticationMethod").doesNotExist())
.andExpect(jsonPath("$.type", is("status")));
}

View File

@@ -7,7 +7,9 @@
*/
package org.dspace.app.rest;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -47,9 +49,16 @@ public class ShibbolethRestControllerIT extends AbstractControllerIntegrationTes
// unauthenticated, but it must include some expected SHIB attributes.
// SHIB-MAIL attribute is the default email header sent from Shibboleth after a successful login.
// In this test we are simply mocking that behavior by setting it to an existing EPerson.
getClient().perform(get("/api/authn/shibboleth").requestAttr("SHIB-MAIL", eperson.getEmail()))
String token = getClient().perform(get("/api/authn/shibboleth").requestAttr("SHIB-MAIL", eperson.getEmail()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:4000"));
.andExpect(redirectedUrl("http://localhost:4000"))
.andReturn().getResponse().getHeader("Authorization");
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.authenticationMethod", is("shibboleth")));
}
@Test