made JTW expiration configurable

This commit is contained in:
2019-02-18 22:17:52 +01:00
parent 70bebd4ef8
commit 7b315f6fd0

View File

@ -1,276 +1,297 @@
/* /*
* Copyright 2018 joern.muehlencord. * Copyright 2018 joern.muehlencord.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package de.muehlencord.shared.account.business.account.boundary; package de.muehlencord.shared.account.business.account.boundary;
import de.muehlencord.shared.account.business.account.control.AccountControl; import de.muehlencord.shared.account.business.account.control.AccountControl;
import de.muehlencord.shared.account.business.account.entity.AccountEntity; import de.muehlencord.shared.account.business.account.entity.AccountEntity;
import de.muehlencord.shared.account.business.account.entity.ApiKeyEntity; import de.muehlencord.shared.account.business.account.entity.ApiKeyEntity;
import de.muehlencord.shared.account.business.account.entity.JWTObject; import de.muehlencord.shared.account.business.account.entity.ApiKeyObject;
import de.muehlencord.shared.account.business.config.boundary.ConfigService; import de.muehlencord.shared.account.business.account.entity.JWTObject;
import de.muehlencord.shared.account.business.config.entity.ConfigException; import de.muehlencord.shared.account.business.config.boundary.ConfigService;
import de.muehlencord.shared.account.util.AccountPU; import de.muehlencord.shared.account.business.config.entity.ConfigException;
import de.muehlencord.shared.jeeutil.jwt.JWTDecoder; import de.muehlencord.shared.account.util.AccountPU;
import de.muehlencord.shared.jeeutil.jwt.JWTEncoder; import de.muehlencord.shared.jeeutil.jwt.JWTDecoder;
import de.muehlencord.shared.jeeutil.jwt.JWTException; import de.muehlencord.shared.jeeutil.jwt.JWTEncoder;
import de.muehlencord.shared.util.DateUtil; import de.muehlencord.shared.jeeutil.jwt.JWTException;
import de.muehlencord.shared.util.StringUtil; import de.muehlencord.shared.util.DateUtil;
import java.io.Serializable; import de.muehlencord.shared.util.StringUtil;
import java.time.ZoneId; import java.io.Serializable;
import java.time.ZoneOffset; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZoneOffset;
import java.util.ArrayList; import java.time.ZonedDateTime;
import java.util.Date; import java.util.ArrayList;
import java.util.Iterator; import java.util.Date;
import java.util.List; import java.util.Iterator;
import javax.annotation.PostConstruct; import java.util.List;
import javax.ejb.Lock; import javax.annotation.PostConstruct;
import javax.ejb.LockType; import javax.ejb.Lock;
import javax.ejb.Stateless; import javax.ejb.LockType;
import javax.ejb.TransactionAttribute; import javax.ejb.Stateless;
import javax.ejb.TransactionAttributeType; import javax.ejb.TransactionAttribute;
import javax.inject.Inject; import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager; import javax.inject.Inject;
import javax.persistence.Query; import javax.persistence.EntityManager;
import javax.transaction.Transactional; import javax.persistence.Query;
import org.apache.commons.lang3.RandomStringUtils; import javax.transaction.Transactional;
import org.slf4j.Logger; import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.LoggerFactory; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* /**
* @author Joern Muehlencord <joern at muehlencord.de> *
*/ * @author Joern Muehlencord <joern at muehlencord.de>
@Stateless */
public class ApiKeyService implements Serializable { @Stateless
public class ApiKeyService implements Serializable {
private static final long serialVersionUID = -6981864888118320228L;
private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyService.class); private static final long serialVersionUID = -6981864888118320228L;
private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyService.class);
@Inject
@AccountPU @Inject
EntityManager em; @AccountPU
EntityManager em;
@Inject
AccountControl accountControl; @Inject
AccountControl accountControl;
@Inject
ConfigService configService; @Inject
ConfigService configService;
private String password;
private String issuer; private String password;
private String issuer;
@PostConstruct private Short expirationInMinutes;
public void init() {
if (configService == null) { @PostConstruct
password = null; public void init() {
issuer = null; if (configService == null) {
} else { password = null;
try { issuer = null;
password = configService.getConfigValue("rest.password"); } else {
issuer = configService.getConfigValue("rest.issuer"); try {
} catch (ConfigException ex) { password = configService.getConfigValue("rest.password");
if (LOGGER.isDebugEnabled()) { issuer = configService.getConfigValue("rest.issuer");
LOGGER.debug(ex.toString(), ex); expirationInMinutes = Short.parseShort(configService.getConfigValue("rest.expiration_in_minutes", "120", true));
} else { } catch (ConfigException | NumberFormatException ex) {
LOGGER.error(ex.toString()); if (LOGGER.isDebugEnabled()) {
} LOGGER.debug(ex.toString(), ex);
password = null; } else {
issuer = null; LOGGER.error(ex.toString());
} }
password = null;
} issuer = null;
} expirationInMinutes = null;
}
public ApiKeyEntity getApiKeyFromString(String encodedJWT) throws ApiKeyException {
if (StringUtil.isEmpty(encodedJWT)) { }
throw new ApiKeyException("Must provide authorization information"); }
}
JWTObject jwt = getJWTObject(encodedJWT); public ApiKeyEntity getApiKeyFromString(String encodedJWT) throws ApiKeyException {
Query query = em.createNamedQuery("ApiKeyEntity.findByApiKey"); if (StringUtil.isEmpty(encodedJWT)) {
query.setParameter("apiKey", jwt.getUnqiueId()); throw new ApiKeyException("Must provide authorization information");
List<ApiKeyEntity> apiKeys = query.getResultList(); }
if ((apiKeys == null) || (apiKeys.isEmpty())) { JWTObject jwt = getJWTObject(encodedJWT);
throw new ApiKeyException("ApiKey not found in database"); Query query = em.createNamedQuery("ApiKeyEntity.findByApiKey");
} query.setParameter("apiKey", jwt.getUnqiueId());
return apiKeys.get(0); List<ApiKeyEntity> apiKeys = query.getResultList();
} if ((apiKeys == null) || (apiKeys.isEmpty())) {
throw new ApiKeyException("ApiKey not found in database");
public List<ApiKeyEntity> getUsersApiKeys(AccountEntity account) { }
Query query = em.createNamedQuery("ApiKeyEntity.findByAccount"); return apiKeys.get(0);
query.setParameter("account", account); }
List<ApiKeyEntity> keys = query.getResultList();
if (keys == null) { public List<ApiKeyEntity> getUsersApiKeys(AccountEntity account) {
return new ArrayList<>(); Query query = em.createNamedQuery("ApiKeyEntity.findByAccount");
} else { query.setParameter("account", account);
return keys; List<ApiKeyEntity> keys = query.getResultList();
} if (keys == null) {
return new ArrayList<>();
} } else {
return keys;
public List<ApiKeyEntity> getUsersApiKeys(String userName) { }
return getUsersApiKeys(accountControl.getAccountEntity(userName, false));
} }
@Transactional public List<ApiKeyEntity> getUsersApiKeys(String userName) {
@Lock(LockType.WRITE) return getUsersApiKeys(accountControl.getAccountEntity(userName, false));
public String createNewApiKey(String userName, short expirationInMinutes) throws ApiKeyException { }
if ((password == null || issuer == null)) {
LOGGER.error("password or issuer not set in, please validate configuration"); @Transactional
} @Lock(LockType.WRITE)
Date now = DateUtil.getCurrentTimeInUTC(); public ApiKeyObject createNewApiKey(String userName) throws ApiKeyException {
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.of("UTC")); return createNewApiKey(userName, expirationInMinutes);
String apiKeyString = RandomStringUtils.randomAscii(50); }
ApiKeyEntity apiKey = new ApiKeyEntity(); @Transactional
apiKey.setAccount(accountControl.getAccountEntity(userName, false)); @Lock(LockType.WRITE)
apiKey.setApiKey(apiKeyString); public ApiKeyObject createNewApiKey(String userName, short expirationInMinutes) throws ApiKeyException {
apiKey.setIssuedOn(now); if ((password == null || issuer == null)) {
apiKey.setExpiration(expirationInMinutes); LOGGER.error("password or issuer not set in, please validate configuration");
}
try { Date now = DateUtil.getCurrentTimeInUTC();
String jwtString = JWTEncoder.encode(password, issuer, zonedDateTime, apiKey.getAccount().getUsername(), apiKey.getApiKey(), apiKey.getExpiration()); ZonedDateTime issuedOn = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.of("UTC"));
em.persist(apiKey); ZonedDateTime expiresOn = issuedOn.plusMinutes(expirationInMinutes);
return jwtString; String apiKeyString = RandomStringUtils.randomAscii(50);
} catch (JWTException ex) {
throw new ApiKeyException("Cannot create apiKey. Reason: " + ex.toString(), ex); ApiKeyEntity apiKey = new ApiKeyEntity();
} apiKey.setAccount(accountControl.getAccountEntity(userName, false));
} apiKey.setApiKey(apiKeyString);
apiKey.setIssuedOn(now);
public boolean validateJWT(String encodedJWT) { apiKey.setExpiration(expirationInMinutes);
JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT);
ApiKeyEntity validKey; try {
try { String jwtString = JWTEncoder.encode(password, issuer, issuedOn, apiKey.getAccount().getUsername(), apiKey.getApiKey(), apiKey.getExpiration());
validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); em.persist(apiKey);
} catch (JWTException ex) { if (LOGGER.isDebugEnabled()) {
if (LOGGER.isTraceEnabled()) { LOGGER.debug("Created API key for {}, valid for {} minutes", userName, expirationInMinutes);
LOGGER.trace(ex.toString(), ex); }
}
return false; ApiKeyObject apiKeyObject = new ApiKeyObject();
} apiKeyObject.setUserName(userName);
return validKey != null; apiKeyObject.setIssuedOn(Date.from(apiKey.getIssuedOn().toInstant()));
} apiKeyObject.setExpiresOn(Date.from(expiresOn.toInstant()));
apiKeyObject.setAuthToken(jwtString);
private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) throws JWTException {
AccountEntity userAccount = accountControl.getAccountEntity(userName, false); return apiKeyObject;
if (userAccount == null) { } catch (JWTException ex) {
throw new JWTException("AccountControl exception"); throw new ApiKeyException("Cannot create apiKey. Reason: " + ex.toString(), ex);
} }
List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount); }
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName); public boolean validateJWT(String encodedJWT) {
} JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT);
ApiKeyEntity validKey;
Iterator<ApiKeyEntity> it = apiKeys.iterator(); try {
ApiKeyEntity keyToLogout = null; validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT);
while (keyToLogout == null && it.hasNext()) { } catch (JWTException ex) {
ApiKeyEntity key = it.next(); if (LOGGER.isTraceEnabled()) {
if (key.getApiKey().equals(apiKey)) { LOGGER.trace(ex.toString(), ex);
if (LOGGER.isTraceEnabled()) { }
LOGGER.trace("Found API key in database"); return false;
} }
return validKey != null;
ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC); }
String testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration());
if (LOGGER.isTraceEnabled()) { private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) throws JWTException {
LOGGER.trace("Successfully created validation JWT for user {}", userName); AccountEntity userAccount = accountControl.getAccountEntity(userName, false);
} if (userAccount == null) {
throw new JWTException("AccountControl exception");
if (authorizationHeader.equals(testString)) { }
if (LOGGER.isDebugEnabled()) { List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount);
LOGGER.debug("Found valid key for user {}", userName); if (LOGGER.isTraceEnabled()) {
} LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName);
return key; }
}
} Iterator<ApiKeyEntity> it = apiKeys.iterator();
} ApiKeyEntity keyToLogout = null;
if (LOGGER.isDebugEnabled()) { while (keyToLogout == null && it.hasNext()) {
LOGGER.debug("No valid key for user {} found", userName); ApiKeyEntity key = it.next();
} if (key.getApiKey().equals(apiKey)) {
return null; if (LOGGER.isTraceEnabled()) {
} LOGGER.trace("Found API key in database");
}
public String getJWTFromApiKey(ApiKeyEntity apiKey) throws ApiKeyException {
ZonedDateTime issuedAt = ZonedDateTime.ofInstant(apiKey.getIssuedOn().toInstant(), ZoneOffset.UTC); ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC);
try { String testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration());
return JWTEncoder.encode(password, issuer, issuedAt, apiKey.getAccount().getUsername(), apiKey.getApiKey(), apiKey.getExpiration()); if (LOGGER.isTraceEnabled()) {
} catch (JWTException ex) { LOGGER.trace("Successfully created validation JWT for user {}", userName);
throw new ApiKeyException("Cannot retrieve JWT from key. Reason: " + ex.toString(), ex); }
}
} if (authorizationHeader.equals(testString)) {
if (LOGGER.isDebugEnabled()) {
public JWTObject getJWTObject(String encodedJWT) { LOGGER.debug("Found valid key for user {}", userName);
JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT); }
JWTObject jwtObject = new JWTObject(); return key;
jwtObject.setUserName(decoder.getSubject()); }
jwtObject.setUnqiueId(decoder.getUniqueId()); }
jwtObject.setValid(true); }
return jwtObject; if (LOGGER.isDebugEnabled()) {
} LOGGER.debug("No valid key for user {} found", userName);
}
/** return null;
* }
* @param apiKey
* @deprecated use delete (jwtObject) instead public String getJWTFromApiKey(ApiKeyEntity apiKey) throws ApiKeyException {
*/ ZonedDateTime issuedAt = ZonedDateTime.ofInstant(apiKey.getIssuedOn().toInstant(), ZoneOffset.UTC);
@TransactionAttribute(TransactionAttributeType.REQUIRED) try {
@Transactional return JWTEncoder.encode(password, issuer, issuedAt, apiKey.getAccount().getUsername(), apiKey.getApiKey(), apiKey.getExpiration());
@Lock(LockType.WRITE) } catch (JWTException ex) {
@Deprecated throw new ApiKeyException("Cannot retrieve JWT from key. Reason: " + ex.toString(), ex);
public void delete(ApiKeyEntity apiKey) { }
em.remove(em.merge(apiKey)); }
}
public JWTObject getJWTObject(String encodedJWT) {
@TransactionAttribute(TransactionAttributeType.REQUIRED) JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT);
@Transactional JWTObject jwtObject = new JWTObject();
@Lock(LockType.WRITE) jwtObject.setUserName(decoder.getSubject());
public void delete(String authorizationHeader) throws ApiKeyException { jwtObject.setUnqiueId(decoder.getUniqueId());
jwtObject.setValid(true);
JWTObject jwtObject = getJWTObject(authorizationHeader); return jwtObject;
if (jwtObject.isValid()) { }
String userName = jwtObject.getUserName();
/**
ApiKeyEntity keyToLogout; *
try { * @param apiKey
keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader); * @deprecated use delete (jwtObject) instead
} catch (JWTException ex) { */
if (LOGGER.isTraceEnabled()) { @TransactionAttribute(TransactionAttributeType.REQUIRED)
LOGGER.trace(ex.getMessage(), ex); @Transactional
} @Lock(LockType.WRITE)
@Deprecated
keyToLogout = null; public void delete(ApiKeyEntity apiKey) {
} em.remove(em.merge(apiKey));
}
if (keyToLogout == null) {
// no valid key found - must not happen, JWTVeryfingFIlter should have catched this @TransactionAttribute(TransactionAttributeType.REQUIRED)
// FIXME - add logging / handle this problem @Transactional
if (LOGGER.isDebugEnabled()) { @Lock(LockType.WRITE)
LOGGER.debug("No valid key found, probably user {} has already logged out", userName); public void delete(String authorizationHeader) throws ApiKeyException {
}
throw new ApiKeyException("No valid key found, probably user " + userName + " has already logged out"); JWTObject jwtObject = getJWTObject(authorizationHeader);
} else { if (jwtObject.isValid()) {
if (LOGGER.isDebugEnabled()) { String userName = jwtObject.getUserName();
LOGGER.debug("Found matching apiKey to logout");
} ApiKeyEntity keyToLogout;
em.remove(em.merge(keyToLogout)); try {
if (LOGGER.isDebugEnabled()) { keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader);
LOGGER.debug("Key deleted, user {} logged out from webservice", userName); } catch (JWTException ex) {
} if (LOGGER.isTraceEnabled()) {
} LOGGER.trace(ex.getMessage(), ex);
} else { }
throw new ApiKeyException("Provided JWT is not valid");
} keyToLogout = null;
} }
}
if (keyToLogout == null) {
// no valid key found - must not happen, JWTVeryfingFIlter should have catched this
// FIXME - add logging / handle this problem
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("No valid key found, probably user {} has already logged out", userName);
}
throw new ApiKeyException("No valid key found, probably user " + userName + " has already logged out");
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Found matching apiKey to logout");
}
em.remove(em.merge(keyToLogout));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Key deleted, user {} logged out from webservice", userName);
}
}
} else {
throw new ApiKeyException("Provided JWT is not valid");
}
}
}