71342: Authorization for Downloads of restricted Bitstreams #1

This commit is contained in:
Peter Nijs
2020-06-12 13:56:38 +02:00
parent e03a3f47b3
commit 48a8639902
14 changed files with 410 additions and 37 deletions

View File

@@ -16,9 +16,11 @@ import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.converter.EPersonConverter;
import org.dspace.app.rest.link.HalLinkService;
import org.dspace.app.rest.model.AuthenticationStatusRest;
import org.dspace.app.rest.model.AuthenticationTokenRest;
import org.dspace.app.rest.model.AuthnRest;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource;
import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource;
import org.dspace.app.rest.model.hateoas.AuthnResource;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.security.RestAuthenticationService;
@@ -32,6 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@@ -118,6 +121,30 @@ public class AuthenticationRestController implements InitializingBean {
"valid.");
}
/**
* This method will generate a short lived token to be used for bitstream downloads.
*
* curl -v -X POST https://{dspace-server.url}/api/authn/shortlivedtokens -H "Authorization: Bearer eyJhbG...COdbo"
*
* Example:
* <pre>
* {@code
* curl -v -X POST https://{dspace-server.url}/api/authn/shortlivedtokens -H "Authorization: Bearer eyJhbG...COdbo"
* }
* </pre>
* @param request The StandardMultipartHttpServletRequest
* @return The created short lived token
*/
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@RequestMapping(value = "/shortlivedtokens", method = RequestMethod.POST)
public AuthenticationTokenResource shortLivedLogin(HttpServletRequest request) {
Projection projection = utils.obtainProjection();
String shortLivedToken =
restAuthenticationService.getShortLivedAuthenticationToken(ContextUtil.obtainContext(request), request);
AuthenticationTokenRest authenticationTokenRest = converter.toRest(shortLivedToken, projection);
return converter.toResource(authenticationTokenRest);
}
@RequestMapping(value = "/login", method = { RequestMethod.GET, RequestMethod.PUT, RequestMethod.PATCH,
RequestMethod.DELETE })
public ResponseEntity login() {

View File

@@ -0,0 +1,30 @@
/**
* 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.converter;
import org.dspace.app.rest.model.AuthenticationTokenRest;
import org.dspace.app.rest.projection.Projection;
import org.springframework.stereotype.Component;
/**
* This is the converter from the AuthenticationToken string to tge REST data model
*/
@Component
public class AuthenticationTokenConverter implements DSpaceConverter<String, AuthenticationTokenRest> {
@Override
public AuthenticationTokenRest convert(String modelObject, Projection projection) {
AuthenticationTokenRest token = new AuthenticationTokenRest();
token.setToken(modelObject);
return token;
}
@Override
public Class<String> getModelClass() {
return String.class;
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.link;
import java.util.LinkedList;
import org.dspace.app.rest.AuthenticationRestController;
import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Component;
/**
* This class adds the self link to the AuthenticationTokenResource.
*/
@Component
public class AuthenticationTokenHalLinkFactory
extends HalLinkFactory<AuthenticationTokenResource, AuthenticationRestController> {
@Override
protected void addLinks(AuthenticationTokenResource halResource, Pageable pageable, LinkedList<Link> list)
throws Exception {
list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().shortLivedLogin(null)));
}
@Override
protected Class<AuthenticationRestController> getControllerClass() {
return AuthenticationRestController.class;
}
@Override
protected Class<AuthenticationTokenResource> getResourceClass() {
return AuthenticationTokenResource.class;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.model;
import org.dspace.app.rest.RestResourceController;
/**
* The authentication token REST HAL Resource. The HAL Resource wraps the REST Resource
* adding support for the links and embedded resources
*/
public class AuthenticationTokenRest extends RestAddressableModel {
public static final String NAME = "shortlivedtoken";
public static final String CATEGORY = "authn";
private String token;
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class getController() {
return RestResourceController.class;
}
@Override
public String getType() {
return NAME;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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.model.hateoas;
import org.dspace.app.rest.model.AuthenticationTokenRest;
/**
* Token resource, wraps the AuthenticationToken object
*/
public class AuthenticationTokenResource extends HALResource<AuthenticationTokenRest> {
public AuthenticationTokenResource(AuthenticationTokenRest content) {
super(content);
}
}

View File

@@ -10,7 +10,7 @@ package org.dspace.app.rest.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.app.rest.security.jwt.JWTTokenHandler;
import org.dspace.app.rest.security.jwt.SessionJWTTokenHandler;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.slf4j.Logger;
@@ -29,7 +29,7 @@ import org.springframework.stereotype.Component;
@Component
public class CustomLogoutHandler implements LogoutHandler {
private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class);
private static final Logger log = LoggerFactory.getLogger(SessionJWTTokenHandler.class);
@Autowired
private RestAuthenticationService restAuthenticationService;

View File

@@ -28,6 +28,8 @@ public interface RestAuthenticationService {
void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response,
DSpaceAuthentication authentication, boolean addCookie) throws IOException;
String getShortLivedAuthenticationToken(Context context, HttpServletRequest request);
EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context);
boolean hasAuthenticationData(HttpServletRequest request);

View File

@@ -46,17 +46,16 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Component;
/**
* Class responsible for creating and parsing JSON Web Tokens (JWTs), supports both JWS and JWE
* https://jwt.io/
* https://jwt.io/ . This abstract class needs to be extended with a class providing the
* configuration keys for the particular type of token.
*
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Tom Desair (tom dot desair at atmire dot com)
*/
@Component
public class JWTTokenHandler implements InitializingBean {
public abstract class JWTTokenHandler implements InitializingBean {
private static final int MAX_CLOCK_SKEW_SECONDS = 60;
private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class);
@@ -86,15 +85,57 @@ public class JWTTokenHandler implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
this.jwtKey = getSecret("jwt.token.secret");
this.encryptionKey = getSecret("jwt.encryption.secret").getBytes();
this.jwtKey =
getSecret(getTokenSecretConfigurationKey());
this.encryptionKey =
getSecret(getEncryptionSecretConfigurationKey()).getBytes();
this.expirationTime = configurationService.getLongProperty("jwt.token.expiration", 30) * 60 * 1000;
this.includeIP = configurationService.getBooleanProperty("jwt.token.include.ip", true);
this.encryptionEnabled = configurationService.getBooleanProperty("jwt.encryption.enabled", false);
this.compressionEnabled = configurationService.getBooleanProperty("jwt.compression.enabled", false);
this.expirationTime =
configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 30);
this.includeIP =
configurationService.getBooleanProperty(getTokenIncludeIPConfigurationKey(), true);
this.encryptionEnabled =
configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false);
this.compressionEnabled =
configurationService.getBooleanProperty(getCompressionEnabledConfigurationKey(), false);
}
/**
* Get the configuration property key for the token secret.
* @return the configuration property key
*/
protected abstract String getTokenSecretConfigurationKey();
/**
* Get the configuration property key for the encryption secret.
* @return the configuration property key
*/
protected abstract String getEncryptionSecretConfigurationKey();
/**
* Get the configuration property key for the expiration time.
* @return the configuration property key
*/
protected abstract String getTokenExpirationConfigurationKey();
/**
* Get the configuration property key for the include ip.
* @return the configuration property key
*/
protected abstract String getTokenIncludeIPConfigurationKey();
/**
* Get the configuration property key for the encryption enable setting.
* @return the configuration property key
*/
protected abstract String getEncryptionEnabledConfigurationKey();
/**
* Get the configuration property key for the compression enable setting.
* @return the configuration property key
*/
protected abstract String getCompressionEnabledConfigurationKey();
/**
* Retrieve EPerson from a JSON Web Token (JWT)
*

View File

@@ -49,7 +49,10 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
private static final String AUTHORIZATION_TYPE = "Bearer";
@Autowired
private JWTTokenHandler jwtTokenHandler;
private SessionJWTTokenHandler sessionJWTTokenHandler;
@Autowired
private ShortLivedJWTTokenHandler shortLivedJWTTokenHandler;
@Autowired
private EPersonService ePersonService;
@@ -71,7 +74,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
List<Group> groups = authenticationService.getSpecialGroups(context, request);
String token = jwtTokenHandler.createTokenForEPerson(context, request,
String token = sessionJWTTokenHandler.createTokenForEPerson(context, request,
authentication.getPreviousLoginDate(), groups);
addTokenToResponse(response, token, addCookie);
@@ -84,11 +87,34 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
}
}
/**
* Create a short-lived token for bitstream downloads
* @param context The context for which to create the token
* @param request The request for which to create the token
* @return The token with a short lifespan
*/
@Override
public String getShortLivedAuthenticationToken(Context context, HttpServletRequest request) {
String token = null;
try {
List<Group> groups = authenticationService.getSpecialGroups(context, request);
token = shortLivedJWTTokenHandler.createTokenForEPerson(context, request, null, groups);
context.commit();
return token;
} catch (JOSEException e) {
log.error("JOSE Exception", e);
} catch (SQLException e) {
log.error("SQL error when adding authentication", e);
}
return token;
}
@Override
public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) {
String token = getToken(request);
try {
EPerson ePerson = jwtTokenHandler.parseEPersonFromToken(token, request, context);
EPerson ePerson = sessionJWTTokenHandler.parseEPersonFromToken(token, request, context);
return ePerson;
} catch (JOSEException e) {
log.error("Jose error", e);
@@ -111,7 +137,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
Context context) throws Exception {
String token = getToken(request);
invalidateAuthenticationCookie(response);
jwtTokenHandler.invalidateToken(token, request, context);
sessionJWTTokenHandler.invalidateToken(token, request, context);
}
@Override

View File

@@ -0,0 +1,47 @@
/**
* 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 org.springframework.stereotype.Component;
/**
* Class responsible for creating and parsing JSON Web Tokens (JWTs), supports both JWS and JWE
* https://jwt.io/
*/
@Component
public class SessionJWTTokenHandler extends JWTTokenHandler {
@Override
protected String getTokenSecretConfigurationKey() {
return "jwt.session.token.secret";
}
@Override
protected String getEncryptionSecretConfigurationKey() {
return "jwt.session.encryption.secret";
}
@Override
protected String getTokenExpirationConfigurationKey() {
return "jwt.session.token.expiration";
}
@Override
protected String getTokenIncludeIPConfigurationKey() {
return "jwt.session.token.include.ip";
}
@Override
protected String getEncryptionEnabledConfigurationKey() {
return "jwt.session.encryption.enabled";
}
@Override
protected String getCompressionEnabledConfigurationKey() {
return "jwt.session.compression.enabled";
}
}

View File

@@ -0,0 +1,47 @@
/**
* 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 org.springframework.stereotype.Component;
/**
* Class responsible for creating and parsing JSON Web Tokens (JWTs) used for bitstream
* dowloads, supports both JWS and JWE https://jwt.io/ .
*/
@Component
public class ShortLivedJWTTokenHandler extends JWTTokenHandler {
@Override
protected String getTokenSecretConfigurationKey() {
return "jwt.shortLived.token.secret";
}
@Override
protected String getEncryptionSecretConfigurationKey() {
return "jwt.shortLived.encryption.secret";
}
@Override
protected String getTokenExpirationConfigurationKey() {
return "jwt.shortLived.token.expiration";
}
@Override
protected String getTokenIncludeIPConfigurationKey() {
return "jwt.shortLived.token.include.ip";
}
@Override
protected String getEncryptionEnabledConfigurationKey() {
return "jwt.shortLived.encryption.enabled";
}
@Override
protected String getCompressionEnabledConfigurationKey() {
return "jwt.shortLived.compression.enabled";
}
}

View File

@@ -11,6 +11,7 @@ import static java.lang.Thread.sleep;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertNotEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -30,6 +31,7 @@ import org.dspace.app.rest.matcher.HalMatcher;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.eperson.Group;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -757,4 +759,19 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isUnauthorized());
}
@Test
public void testShortLivedToken() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(post("/api/authn/shortlivedtokens"))
.andExpect(jsonPath("$.token", notNullValue()))
.andExpect(jsonPath("$.type", is("shortlivedtoken")))
.andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/authn/shortlivedtokens")));
}
@Test
public void testShortLivedTokenNotAuthenticated() throws Exception {
getClient().perform(post("/api/authn/shortlivedtokens"))
.andExpect(status().isUnauthorized());
}
}

View File

@@ -46,7 +46,7 @@ public class JWTTokenHandlerTest {
@InjectMocks
@Spy
JWTTokenHandler jwtTokenHandler;
SessionJWTTokenHandler sessionJWTTokenHandler;
@Mock
private Context context;
@@ -87,7 +87,7 @@ public class JWTTokenHandlerTest {
@Test
public void testJWTNoEncryption() throws Exception {
Date previous = new Date(System.currentTimeMillis() - 10000000000L);
String token = jwtTokenHandler
String token = sessionJWTTokenHandler
.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
SignedJWT signedJWT = SignedJWT.parse(token);
String personId = (String) signedJWT.getJWTClaimsSet().getClaim(EPersonClaimProvider.EPERSON_ID);
@@ -96,11 +96,11 @@ public class JWTTokenHandlerTest {
@Test(expected = ParseException.class)
public void testJWTEncrypted() throws Exception {
when(jwtTokenHandler.isEncryptionEnabled()).thenReturn(true);
when(sessionJWTTokenHandler.isEncryptionEnabled()).thenReturn(true);
Date previous = new Date(System.currentTimeMillis() - 10000000000L);
StringKeyGenerator keyGenerator = KeyGenerators.string();
when(jwtTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes());
String token = jwtTokenHandler
when(sessionJWTTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes());
String token = sessionJWTTokenHandler
.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
SignedJWT signedJWT = SignedJWT.parse(token);
}
@@ -108,12 +108,12 @@ public class JWTTokenHandlerTest {
//temporary set a negative expiration time so the token is invalid immediately
@Test
public void testExpiredToken() throws Exception {
when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson);
Date previous = new Date(new Date().getTime() - 10000000000L);
String token = jwtTokenHandler
String token = sessionJWTTokenHandler
.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
EPerson parsed = jwtTokenHandler.parseEPersonFromToken(token, httpServletRequest, context);
EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context);
assertEquals(null, parsed);
}
@@ -121,17 +121,17 @@ public class JWTTokenHandlerTest {
//Try if we can change the expiration date
@Test
public void testTokenTampering() throws Exception {
when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson);
Date previous = new Date(new Date().getTime() - 10000000000L);
String token = jwtTokenHandler
String token = sessionJWTTokenHandler
.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().claim("eid", "epersonID").expirationTime(
new Date(System.currentTimeMillis() + 99999999)).build();
String tamperedPayload = new String(Base64.getUrlEncoder().encode(jwtClaimsSet.toString().getBytes()));
String[] splitToken = token.split("\\.");
String tamperedToken = splitToken[0] + "." + tamperedPayload + "." + splitToken[2];
EPerson parsed = jwtTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context);
EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context);
assertEquals(null, parsed);
}
@@ -139,12 +139,12 @@ public class JWTTokenHandlerTest {
public void testInvalidatedToken() throws Exception {
Date previous = new Date(System.currentTimeMillis() - 10000000000L);
// create a new token
String token = jwtTokenHandler
String token = sessionJWTTokenHandler
.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
// immediately invalidate it
jwtTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context);
sessionJWTTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context);
// Check if it is still valid by trying to parse the EPerson from it (should return null)
EPerson parsed = jwtTokenHandler.parseEPersonFromToken(token, httpServletRequest, context);
EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context);
assertEquals(null, parsed);
}

View File

@@ -57,26 +57,56 @@ plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authen
# Server key part that is a part of the key used to sign the authentication tokens.
# If this property is not set or empty, DSpace will generate a random key on startup.
# IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable
# jwt.token.secret =
# jwt.session.token.secret =
# This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted
# and unreadable by the receiver, but makes the token larger in size. false by default
jwt.encryption.enabled = false
jwt.session.encryption.enabled = false
# Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional
# configuration in the REST clients
# jwt.encryption.secret =
# jwt.session.encryption.secret =
# 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, this setting WILL ONLY BE used when encrypting the jwt.
jwt.compression.enabled = true
jwt.session.compression.enabled = true
# Expiration time of a token in minutes
jwt.token.expiration = 30
# Expiration time of a token in milliseconds
jwt.session.token.expiration = 1800000
# Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address
# a part of the JWT siging key. 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 and tokens can be shared over multiple ip-addresses.
# For security reasons, this defaults to true
jwt.token.include.ip = true
jwt.session.token.include.ip = true
#---------------------------------------------------------------#
#---Stateless JWT Authentication for downloads of bitstreams----#
#---------------------------------------------------------------#
# Server key part that is a part of the key used to sign the authentication tokens.
# If this property is not set or empty, DSpace will generate a random key on startup.
# IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable
# jwt.shortLived.token.secret =
# This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted
# and unreadable by the receiver, but makes the token larger in size. false by default
jwt.shortLived.encryption.enabled = false
# Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional
# configuration in the REST clients
# jwt.shortLived.encryption.secret =
# 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, this setting WILL ONLY BE used when encrypting the jwt.
jwt.shortLived.compression.enabled = true
# Expiration time of a token in milliseconds
jwt.shortLived.token.expiration = 2000
# Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address
# a part of the JWT siging key. 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 and tokens can be shared over multiple ip-addresses.
# For security reasons, this defaults to true
jwt.shortLived.token.include.ip = true