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