DS-3542 status endpoint

This commit is contained in:
frederic
2017-10-13 15:00:00 +02:00
committed by Tom Desair
parent 3e366bf442
commit 02c2612889
10 changed files with 187 additions and 50 deletions

View File

@@ -65,8 +65,8 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
@Column(name="salt", length = 32)
private String salt;
@Column(name="jwt_salt", length = 16)
private String jwtSalt;
@Column(name="session_salt", length = 16)
private String sessionSalt;
@Column(name="digest_algorithm", length = 16)
private String digestAlgorithm;
@@ -437,11 +437,11 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
return ePersonService;
}
public String getJwtSalt() {
return jwtSalt;
public String getSessionSalt() {
return sessionSalt;
}
public void setJwtSalt(String jwtSalt) {
this.jwtSalt = jwtSalt;
public void setSessionSalt(String sessionSalt) {
this.sessionSalt = sessionSalt;
}
}

View File

@@ -0,0 +1 @@
alter table eperson add session_salt varchar(16);

View File

@@ -7,12 +7,6 @@
*/
package org.dspace.app.rest;
import java.io.File;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.Filter;
import org.dspace.app.rest.filter.DSpaceRequestContextFilter;
import org.dspace.app.rest.model.hateoas.DSpaceRelProvider;
import org.dspace.app.rest.utils.ApplicationConfig;
@@ -42,6 +36,13 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.File;
/**
* Define the Spring Boot Application settings itself. This class takes the place
* of a web.xml file, and configures all Filters/Listeners as methods (see below).
@@ -126,7 +127,7 @@ public class Application extends SpringBootServletInitializer {
}
@Bean
public RequestContextListener requestContextListener() {
public RequestContextListener requestContextListener(){
return new RequestContextListener();
}

View File

@@ -0,0 +1,47 @@
package org.dspace.app.rest;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.Collection;
@RestController
@RequestMapping("/status")
public class LoginRestController {
@Autowired
private HttpServletRequest request;
@RequestMapping(method = RequestMethod.GET)
public String status() throws SQLException {
Context context = ContextUtil.obtainContext(request);
//context.getDBConnection().setAutoCommit(false); // Disable autocommit.
//TODO this is the implementation from Resource.createContext()
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication != null)
{
Collection<SimpleGrantedAuthority> specialGroups = (Collection<SimpleGrantedAuthority>) authentication.getAuthorities();
for (SimpleGrantedAuthority grantedAuthority : specialGroups) {
context.setSpecialGroup(EPersonServiceFactory.getInstance().getGroupService().findByName(context, grantedAuthority.getAuthority()).getID());
}
context.setCurrentUser(EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, authentication.getName()));
}
EPerson current = context.getCurrentUser();
return "EPerson: " + current.getEmail() + "\nFull name: " + current.getFullName();
}
}

View File

@@ -1,42 +1,114 @@
package org.dspace.app.rest.security;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.core.LogManager;
import org.dspace.eperson.Group;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Component
public class EPersonRestAuthenticationProvider implements AuthenticationProvider{
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
protected AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance().getAuthenticationService();
private static final Logger log = LoggerFactory.getLogger(EPersonRestAuthenticationProvider.class);
@Autowired
private HttpServletRequest request;
// public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// String email = authentication.getName();
// Context context = new Context();
// EPerson ePerson = null;
// try {
// ePerson = ePersonService.findByEmail(context, email);
// } catch (SQLException e) {
// e.printStackTrace();
// }
// if (ePerson != null) {
// String password = authentication.getCredentials().toString();
// if (ePersonService.checkPassword(context, ePerson, password)) {
// return new UsernamePasswordAuthenticationToken(ePerson.getEmail(), password, new ArrayList<>());
// }
// }
// return null;
// }
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
Context context = new Context();
EPerson ePerson = null;
Context context = null;
try {
ePerson = ePersonService.findByEmail(context, email);
} catch (SQLException e) {
e.printStackTrace();
}
if (ePerson != null) {
context = new Context();
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (ePersonService.checkPassword(context, ePerson, password)) {
return new UsernamePasswordAuthenticationToken(ePerson.getEmail(), password, new ArrayList<>());
HttpServletRequest httpServletRequest = request;
List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
int implicitStatus = authenticationService.authenticateImplicit(context, null, null, null, httpServletRequest);
if (implicitStatus == AuthenticationMethod.SUCCESS) {
log.info(LogManager.getHeader(context, "login", "type=implicit"));
addSpecialGroupsToGrantedAuthorityList(context, httpServletRequest, grantedAuthorities);
return new UsernamePasswordAuthenticationToken(name, password, grantedAuthorities);
} else {
int authenticateResult = authenticationService.authenticate(context, name, password, null, httpServletRequest);
if (AuthenticationMethod.SUCCESS == authenticateResult) {
addSpecialGroupsToGrantedAuthorityList(context, httpServletRequest, grantedAuthorities);
log.info(LogManager
.getHeader(context, "login", "type=explicit"));
return new UsernamePasswordAuthenticationToken(name, password, grantedAuthorities);
} else {
log.info(LogManager.getHeader(context, "failed_login", "email="
+ name + ", result="
+ authenticateResult));
throw new BadCredentialsException("Login failed");
}
}
} catch (BadCredentialsException e)
{
throw e;
} catch (Exception e) {
log.error("Error while authenticating in the rest api", e);
} finally {
if (context != null && context.isValid()) {
try {
context.complete();
} catch (SQLException e) {
log.error(e.getMessage() + " occurred while trying to close", e);
}
}
}
return null;
}
protected void addSpecialGroupsToGrantedAuthorityList(Context context, HttpServletRequest httpServletRequest, List<SimpleGrantedAuthority> grantedAuthorities) throws SQLException {
List<Group> groups = authenticationService.getSpecialGroups(context, httpServletRequest);
for (Group group : groups) {
grantedAuthorities.add(new SimpleGrantedAuthority(group.getName()));
}
}
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}

View File

@@ -15,6 +15,7 @@ import org.dspace.eperson.service.EPersonService;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
@@ -27,14 +28,22 @@ public class JWTTokenHandler {
private static String jwtKey = "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest";
private AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance().getAuthenticationService();
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
public static final String EPERSON_ID = "eid";
public static final String SPECIAL_GROUPS = "sg";
public EPerson parseEPersonFromToken(String token) throws JOSEException, ParseException, SQLException {
public EPerson parseEPersonFromToken(String token, HttpServletRequest request) throws JOSEException, ParseException, SQLException {
SignedJWT signedJWT = SignedJWT.parse(token);
Context context = new Context();
EPerson ePerson = ePersonService.find(context, UUID.fromString(signedJWT.getJWTClaimsSet().getClaim("EPersonID").toString()));
JWSVerifier verifier = new MACVerifier(jwtKey + ePerson.getJwtSalt());
if (signedJWT.verify(verifier)) {
EPerson ePerson = ePersonService.find(context, UUID.fromString(signedJWT.getJWTClaimsSet().getClaim(EPERSON_ID).toString()));
String ipAddress = request.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
JWSVerifier verifier = new MACVerifier(jwtKey + ePerson.getSessionSalt() + ipAddress);
//If token is valid and not expired return eperson in token
if (signedJWT.verify(verifier) && new Date().getTime() < signedJWT.getJWTClaimsSet().getExpirationTime().getTime()) {
return ePerson;
} else {
return null;
@@ -42,13 +51,17 @@ public class JWTTokenHandler {
}
public String createTokenForEPerson(Context context, EPerson ePerson, List<Group> groups) throws JOSEException {
public String createTokenForEPerson(Context context, HttpServletRequest request, EPerson ePerson, List<Group> groups) throws JOSEException {
StringKeyGenerator stringKeyGenerator = KeyGenerators.string();
String salt = stringKeyGenerator.generateKey();
JWSSigner signer = new MACSigner(jwtKey + salt);
String ipAddress = request.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
JWSSigner signer = new MACSigner(jwtKey + salt + ipAddress);
List<Integer> groupIds = groups.stream().map(Group::getLegacyId).collect(Collectors.toList());
ePerson.setJwtSalt(salt);
List<String> groupIds = groups.stream().map(group -> group.getID().toString()).collect(Collectors.toList());
ePerson.setSessionSalt(salt);
try {
context.commit();
} catch (SQLException e) {
@@ -56,8 +69,9 @@ public class JWTTokenHandler {
}
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.claim("EPersonID", ePerson.getID().toString())
.claim("special_groups", groupIds)
.claim(EPERSON_ID, ePerson.getID().toString())
.claim(SPECIAL_GROUPS, groupIds)
//TODO Expiration time configurable in config
.expirationTime(new Date(new Date().getTime() + 60 * 1000))
.build();

View File

@@ -32,7 +32,6 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter{
Cookie cookie = WebUtils.getCookie(req,"access_token");
if (cookie == null ) {
chain.doFilter(req, res);
return;
@@ -48,9 +47,9 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter{
if (token != null) {
// parse the token.
EPerson eperson = tokenAuthenticationService.getAuthentication(token);
EPerson eperson = tokenAuthenticationService.getAuthentication(token, request);
if (eperson != null) {
return new UsernamePasswordAuthenticationToken(eperson, null, new ArrayList<>());
return new UsernamePasswordAuthenticationToken(eperson.getEmail(), null, new ArrayList<>());
}
return null;
}

View File

@@ -46,6 +46,7 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
FilterChain chain,
Authentication auth) throws IOException, ServletException {
//TODO every time we log in a new token and salt is created, might need to change this
tokenAuthenticationService.addAuthentication(req, res, auth.getName());
}

View File

@@ -35,7 +35,8 @@ public class TokenAuthenticationService {
EPerson ePerson = ePersonService.findByEmail(ContextUtil.obtainContext(request), email);
Context context = ContextUtil.obtainContext(request);
List<Group> groups = authenticationService.getSpecialGroups(context, request);
String token = jwtTokenHandler.createTokenForEPerson(context, ePerson, groups);
String token = jwtTokenHandler.createTokenForEPerson(context, request, ePerson, groups);
//TODO token is saved in a cookie, but might be better to save it in http header
Cookie cookie = new Cookie("access_token", token);
response.addCookie(cookie);
} catch (JOSEException e) {
@@ -46,9 +47,10 @@ public class TokenAuthenticationService {
}
public EPerson getAuthentication(String token) {
public EPerson getAuthentication(String token, HttpServletRequest request) {
try {
return jwtTokenHandler.parseEPersonFromToken(token);
EPerson ePerson = jwtTokenHandler.parseEPersonFromToken(token, request);
return ePerson;
} catch (JOSEException e) {
e.printStackTrace();
} catch (ParseException e) {

View File

@@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -25,22 +26,21 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
http.headers().cacheControl();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers(HttpMethod.POST, "/").permitAll()
.antMatchers(HttpMethod.POST, "/api").permitAll()
.antMatchers(HttpMethod.GET, "/api").permitAll()
.antMatchers(HttpMethod.POST, "/api/login").permitAll()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.GET, "/status").authenticated()
.anyRequest().authenticated().and()
.and()
.addFilterBefore(new StatelessLoginFilter("/api/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new StatelessLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
// Custom Token based authentication based on the header previously given to the client
.addFilterBefore(new StatelessAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);