mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-17 15:03:18 +00:00
DS-3542 JWT encryption and compression
This commit is contained in:
@@ -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)
|
||||||
|
@@ -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);
|
@@ -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);
|
@@ -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);
|
@@ -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;
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
Reference in New Issue
Block a user