DS-3542 JWT encryption and compression

This commit is contained in:
frederic
2017-11-15 17:17:55 +01:00
parent 2e650d8964
commit bf8d63c1e3
8 changed files with 77 additions and 40 deletions

View File

@@ -65,7 +65,7 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
@Column(name="salt", length = 32) @Column(name="salt", length = 32)
private String salt; private String salt;
@Column(name="session_salt", length = 16) @Column(name="session_salt", length = 32)
private String sessionSalt; private String sessionSalt;
@Column(name="digest_algorithm", length = 16) @Column(name="digest_algorithm", length = 16)

View File

@@ -17,4 +17,4 @@
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication -- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(16); ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -17,4 +17,4 @@
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication -- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(16); ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -17,4 +17,4 @@
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication -- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(16); ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -8,6 +8,7 @@
package org.dspace.app.rest.model; package org.dspace.app.rest.model;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import org.dspace.app.rest.RestResourceController;
import org.dspace.app.util.Util; import org.dspace.app.util.Util;
/** /**
@@ -16,7 +17,7 @@ import org.dspace.app.util.Util;
* Find out your authentication status. * Find out your authentication status.
* *
*/ */
public class StatusRest extends DSpaceObjectRest public class StatusRest extends BaseObjectRest<Integer>
{ {
private String sourceVersion; private String sourceVersion;
@@ -38,6 +39,10 @@ public class StatusRest extends DSpaceObjectRest
return NAME; return NAME;
} }
public Class getController() {
return RestResourceController.class;
}
private EPersonRest ePersonRest; private EPersonRest ePersonRest;
@@ -83,7 +88,7 @@ public class StatusRest extends DSpaceObjectRest
this.apiVersion = apiVersion; this.apiVersion = apiVersion;
} }
@LinkRest(linkClass = EPersonRest.class, name = "eperson") @LinkRest(linkClass = EPersonRest.class, name = "eperson", optional = true)
@JsonIgnore @JsonIgnore
public EPersonRest getEPersonRest() { public EPersonRest getEPersonRest() {
return ePersonRest; return ePersonRest;

View File

@@ -7,23 +7,15 @@
*/ */
package org.dspace.app.rest.security; package org.dspace.app.rest.security;
import java.sql.SQLException; import com.nimbusds.jose.*;
import java.text.ParseException; import com.nimbusds.jose.crypto.DirectDecrypter;
import java.util.Date; import com.nimbusds.jose.crypto.DirectEncrypter;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.util.DateUtils; import com.nimbusds.jwt.util.DateUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
@@ -31,15 +23,21 @@ import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
@Component @Component
public class JWTTokenHandler implements InitializingBean { public class JWTTokenHandler implements InitializingBean {
@@ -51,6 +49,8 @@ public class JWTTokenHandler implements InitializingBean {
private String jwtKey; private String jwtKey;
private long expirationTime; private long expirationTime;
private boolean includeIP;
private boolean compressionEnabled;
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;
@@ -61,12 +61,19 @@ public class JWTTokenHandler implements InitializingBean {
@Autowired @Autowired
private EPersonService ePersonService; private EPersonService ePersonService;
private byte[] encryptionKey;
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
//TODO move properties to authentication module //TODO move properties to authentication module
this.jwtKey = configurationService.getProperty("jwt.token.secret", "defaultjwtkeysecret"); this.jwtKey = configurationService.getProperty("jwt.token.secret", "defaultjwtkeysecret");
this.expirationTime = configurationService.getLongProperty("jwt.token.expiration", 30) * 60 * 1000; this.expirationTime = configurationService.getLongProperty("jwt.token.expiration", 30) * 60 * 1000;
this.includeIP = configurationService.getBooleanProperty("jwt.token.include.ip", true);
this.compressionEnabled = configurationService.getBooleanProperty("jwt.compression.enabled", false);
//TODO Don't reuse this all the time
BytesKeyGenerator keyGen = KeyGenerators.secureRandom(16);
encryptionKey = keyGen.generateKey();
} }
/** /**
@@ -84,7 +91,12 @@ public class JWTTokenHandler implements InitializingBean {
return null; return null;
} }
SignedJWT signedJWT = SignedJWT.parse(token); JWEObject jweObject = JWEObject.parse(token);
jweObject.decrypt(new DirectDecrypter(encryptionKey));
SignedJWT signedJWT = jweObject.getPayload().toSignedJWT();
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet(); JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
EPerson ePerson = getEPerson(context, jwtClaimsSet); EPerson ePerson = getEPerson(context, jwtClaimsSet);
@@ -144,9 +156,29 @@ public class JWTTokenHandler implements InitializingBean {
signedJWT.sign(signer); signedJWT.sign(signer);
return signedJWT.serialize(); JWEObject jweObject = new JWEObject(
compression(new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A128GCM)
.contentType("JWT"))
.build(), new Payload(signedJWT)
);
jweObject.encrypt(new DirectEncrypter(encryptionKey));
return jweObject.serialize();
} }
//This method makes compression configurable
private JWEHeader.Builder compression(JWEHeader.Builder builder) {
if (compressionEnabled) {
return builder.compressionAlgorithm(CompressionAlgorithm.DEF);
}
return builder;
}
public void invalidateToken(String token, HttpServletRequest request, Context context) { public void invalidateToken(String token, HttpServletRequest request, Context context) {
if (StringUtils.isNotBlank(token)) { if (StringUtils.isNotBlank(token)) {
try { try {
@@ -161,12 +193,14 @@ public class JWTTokenHandler implements InitializingBean {
} }
private String buildSigningKey(HttpServletRequest request, EPerson ePerson) { private String buildSigningKey(HttpServletRequest request, EPerson ePerson) {
String ipAddress = getIpAddress(request); String ipAddress = "";
if (includeIP) {
ipAddress = getIpAddress(request);
}
return jwtKey + ePerson.getSessionSalt() + ipAddress; return jwtKey + ePerson.getSessionSalt() + ipAddress;
} }
private String getIpAddress(HttpServletRequest request) { private String getIpAddress(HttpServletRequest request) {
//TODO FREDERIC make using the ip address of the request optional
String ipAddress = request.getHeader("X-FORWARDED-FOR"); String ipAddress = request.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) { if (ipAddress == null) {
ipAddress = request.getRemoteAddr(); ipAddress = request.getRemoteAddr();
@@ -184,30 +218,24 @@ public class JWTTokenHandler implements InitializingBean {
//If the previous login was within the configured token expiration time, we reuse the session salt. //If the previous login was within the configured token expiration time, we reuse the session salt.
//This allows a user to login on multiple devices/browsers at the same time. //This allows a user to login on multiple devices/browsers at the same time.
if (previousLoginDate == null || (ePerson.getLastActive().getTime() - previousLoginDate.getTime() > expirationTime)) { if (previousLoginDate == null || (ePerson.getLastActive().getTime() - previousLoginDate.getTime() > expirationTime)) {
ePerson.setSessionSalt(generateRandomSalt());
StringKeyGenerator stringKeyGenerator = KeyGenerators.string();
String salt = stringKeyGenerator.generateKey();
ePerson.setSessionSalt(salt);
ePersonService.update(context, ePerson); ePersonService.update(context, ePerson);
} }
} catch (SQLException e) { } catch (SQLException e) {
//TODO FREDERIC: fail fast return null;
e.printStackTrace();
} catch (AuthorizeException e) { } catch (AuthorizeException e) {
//TODO FREDERIC: fail fast return null;
e.printStackTrace();
} }
return ePerson; return ePerson;
} }
public List<JWTClaimProvider> getJwtClaimProviders() { //Generate a random 32 byte salt
return jwtClaimProviders; private String generateRandomSalt() {
BytesKeyGenerator bytesKeyGenerator = KeyGenerators.secureRandom(32);
byte[] secretKey = bytesKeyGenerator.generateKey();
return Base64.encodeBase64String(secretKey);
} }
public void setJwtClaimProviders(List<JWTClaimProvider> jwtClaimProviders) {
this.jwtClaimProviders = jwtClaimProviders;
}
} }

View File

@@ -66,7 +66,6 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
authentication.getPreviousLoginDate(), groups); authentication.getPreviousLoginDate(), groups);
addTokenToResponse(response, token); addTokenToResponse(response, token);
context.commit(); context.commit();
} catch (JOSEException e) { } catch (JOSEException e) {

View File

@@ -53,4 +53,9 @@ plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authen
# Partial key that is used to sign the authentication tokens # Partial key that is used to sign the authentication tokens
jwt.token.secret = thisisatestsecretkeyforjwttokens jwt.token.secret = thisisatestsecretkeyforjwttokens
# Expiration time of a token in minutes # Expiration time of a token in minutes
jwt.token.expiration = 30 jwt.token.expiration = 30
# If this property is set to false then the ip-address won't be used as part of the signing key of a jwt token.
# This defaults to true
# jwt.token.include.ip = true
# This enables compression of the payload of a jwt, enabling this will make the jwt token a little smaller at the cost of some performance
#jwt.compression.enabled = true