added checks to avoid NPEs

This commit is contained in:
2019-04-29 08:13:27 +02:00
parent 4f3c3d4673
commit 59969ccef4

View File

@ -1,328 +1,338 @@
/* /*
* 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.JWTObject;
import de.muehlencord.shared.account.business.config.boundary.ConfigService; import de.muehlencord.shared.account.business.config.boundary.ConfigService;
import de.muehlencord.shared.account.business.config.entity.ConfigException; import de.muehlencord.shared.account.business.config.entity.ConfigException;
import de.muehlencord.shared.account.dao.ApiKeyObject; import de.muehlencord.shared.account.dao.ApiKeyObject;
import de.muehlencord.shared.account.util.AccountPU; import de.muehlencord.shared.account.util.AccountPU;
import de.muehlencord.shared.jeeutil.jwt.JWTDecoder; import de.muehlencord.shared.jeeutil.jwt.JWTDecoder;
import de.muehlencord.shared.jeeutil.jwt.JWTEncoder; import de.muehlencord.shared.jeeutil.jwt.JWTEncoder;
import de.muehlencord.shared.jeeutil.jwt.JWTException; import de.muehlencord.shared.jeeutil.jwt.JWTException;
import de.muehlencord.shared.util.DateUtil; import de.muehlencord.shared.util.DateUtil;
import de.muehlencord.shared.util.StringUtil; import de.muehlencord.shared.util.StringUtil;
import java.io.Serializable; import java.io.Serializable;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.ejb.Lock; import javax.ejb.Lock;
import javax.ejb.LockType; import javax.ejb.LockType;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType; import javax.ejb.TransactionAttributeType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* *
* @author Joern Muehlencord <joern at muehlencord.de> * @author Joern Muehlencord <joern at muehlencord.de>
*/ */
@Stateless @Stateless
public class ApiKeyService implements Serializable { public class ApiKeyService implements Serializable {
private static final long serialVersionUID = -6981864888118320228L; private static final long serialVersionUID = -6981864888118320228L;
private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyService.class); private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyService.class);
@Inject @Inject
@AccountPU @AccountPU
EntityManager em; EntityManager em;
@Inject @Inject
AccountControl accountControl; AccountControl accountControl;
@Inject @Inject
ConfigService configService; ConfigService configService;
private String password; private String password;
private String issuer; private String issuer;
private Short expirationInMinutes; private Short expirationInMinutes;
@PostConstruct @PostConstruct
public void init() { public void init() {
if (configService == null) { if (configService == null) {
password = null; password = null;
issuer = null; issuer = null;
} else { } else {
try { try {
password = configService.getConfigValue("rest.password"); password = configService.getConfigValue("rest.password");
issuer = configService.getConfigValue("rest.issuer"); issuer = configService.getConfigValue("rest.issuer");
expirationInMinutes = Short.parseShort(configService.getConfigValue("rest.expiration_in_minutes", "120", true)); expirationInMinutes = Short.parseShort(configService.getConfigValue("rest.expiration_in_minutes", "120", true));
} catch (ConfigException | NumberFormatException ex) { } catch (ConfigException | NumberFormatException ex) {
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(ex.toString(), ex); LOGGER.debug(ex.toString(), ex);
} else { } else {
LOGGER.error(ex.toString()); LOGGER.error(ex.toString());
} }
password = null; password = null;
issuer = null; issuer = null;
expirationInMinutes = null; expirationInMinutes = null;
} }
} }
} }
public ApiKeyEntity getApiKeyFromString(String encodedJWT) throws ApiKeyException { public ApiKeyEntity getApiKeyFromString(String encodedJWT) throws ApiKeyException {
if (StringUtil.isEmpty(encodedJWT)) { if (StringUtil.isEmpty(encodedJWT)) {
throw new ApiKeyException("Must provide authorization information"); throw new ApiKeyException("Must provide authorization information");
} }
JWTObject jwt = getJWTObject(encodedJWT); JWTObject jwt = getJWTObject(encodedJWT);
Query query = em.createNamedQuery("ApiKeyEntity.findByApiKey"); Query query = em.createNamedQuery("ApiKeyEntity.findByApiKey");
query.setParameter("apiKey", jwt.getUnqiueId()); query.setParameter("apiKey", jwt.getUnqiueId());
List<ApiKeyEntity> apiKeys = query.getResultList(); List<ApiKeyEntity> apiKeys = query.getResultList();
if ((apiKeys == null) || (apiKeys.isEmpty())) { if ((apiKeys == null) || (apiKeys.isEmpty())) {
throw new ApiKeyException("ApiKey not found in database"); throw new ApiKeyException("ApiKey not found in database");
} }
return apiKeys.get(0); return apiKeys.get(0);
} }
public List<ApiKeyEntity> getUsersApiKeys(AccountEntity account, boolean onlyValid) { public List<ApiKeyEntity> getUsersApiKeys(AccountEntity account, boolean onlyValid) {
Date now = DateUtil.getCurrentTimeInUTC(); Date now = DateUtil.getCurrentTimeInUTC();
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<ApiKeyEntity> cq = cb.createQuery(ApiKeyEntity.class); CriteriaQuery<ApiKeyEntity> cq = cb.createQuery(ApiKeyEntity.class);
Root<ApiKeyEntity> root = cq.from(ApiKeyEntity.class); Root<ApiKeyEntity> root = cq.from(ApiKeyEntity.class);
Predicate accountPredicate = cb.equal(root.get("account"), account); Predicate accountPredicate = cb.equal(root.get("account"), account);
Predicate searchPredicate; Predicate searchPredicate;
if (onlyValid) { if (onlyValid) {
Predicate expiresOnPredicate = cb.greaterThanOrEqualTo(root.get("expiresOn"), now); Predicate expiresOnPredicate = cb.greaterThanOrEqualTo(root.get("expiresOn"), now);
searchPredicate = cb.and(accountPredicate, expiresOnPredicate); searchPredicate = cb.and(accountPredicate, expiresOnPredicate);
} else { } else {
searchPredicate = accountPredicate; searchPredicate = accountPredicate;
} }
cq.where(searchPredicate); cq.where(searchPredicate);
cq.orderBy(cb.desc(root.get("expiresOn"))); cq.orderBy(cb.desc(root.get("expiresOn")));
TypedQuery<ApiKeyEntity> query = em.createQuery(cq); TypedQuery<ApiKeyEntity> query = em.createQuery(cq);
List<ApiKeyEntity> resultList = query.getResultList(); List<ApiKeyEntity> resultList = query.getResultList();
if (resultList == null) { if (resultList == null) {
return new ArrayList<>(); return new ArrayList<>();
} else { } else {
return resultList; return resultList;
} }
} }
public List<ApiKeyEntity> getUsersApiKeys(String userName) { public List<ApiKeyEntity> getUsersApiKeys(String userName) {
return getUsersApiKeys(accountControl.getAccountEntity(userName, false), false); return getUsersApiKeys(accountControl.getAccountEntity(userName, false), false);
} }
public List<ApiKeyEntity> getValidUsersApiKeys(String userName) { public List<ApiKeyEntity> getValidUsersApiKeys(String userName) {
return getUsersApiKeys(accountControl.getAccountEntity(userName, false), true); return getUsersApiKeys(accountControl.getAccountEntity(userName, false), true);
} }
@Transactional @Transactional
@Lock(LockType.WRITE) @Lock(LockType.WRITE)
public ApiKeyObject createNewApiKey(String userName) throws ApiKeyException { public ApiKeyObject createNewApiKey(String userName) throws ApiKeyException {
return createNewApiKey(userName, expirationInMinutes); if (userName == null) {
} throw new ApiKeyException("Username must not be null");
}
@Transactional return createNewApiKey(userName, expirationInMinutes);
@Lock(LockType.WRITE) }
public ApiKeyObject createNewApiKey(String userName, short expirationInMinutes) throws ApiKeyException {
if ((password == null || issuer == null)) { @Transactional
LOGGER.error("password or issuer not set in, please validate configuration"); @Lock(LockType.WRITE)
} public ApiKeyObject createNewApiKey(String userName, short expirationInMinutes) throws ApiKeyException {
Date now = DateUtil.getCurrentTimeInUTC(); if ((password == null || issuer == null) || (userName == null)) {
ZonedDateTime issuedOn = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.of("UTC")); LOGGER.error("password, issuer or username not set in, please validate configuration");
ZonedDateTime expiresOn = issuedOn.plusMinutes(expirationInMinutes); }
Date expiresOnDate = Date.from(expiresOn.toInstant()); Date now = DateUtil.getCurrentTimeInUTC();
String apiKeyString = RandomStringUtils.randomAscii(50); ZonedDateTime issuedOn = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.of("UTC"));
ZonedDateTime expiresOn = issuedOn.plusMinutes(expirationInMinutes);
ApiKeyEntity apiKey = new ApiKeyEntity(); Date expiresOnDate = Date.from(expiresOn.toInstant());
apiKey.setAccount(accountControl.getAccountEntity(userName, false)); String apiKeyString = RandomStringUtils.randomAscii(50);
apiKey.setApiKey(apiKeyString);
apiKey.setIssuedOn(now); ApiKeyEntity apiKey = new ApiKeyEntity();
apiKey.setExpiresOn(expiresOnDate); apiKey.setAccount(accountControl.getAccountEntity(userName, false));
apiKey.setExpiration(expirationInMinutes); apiKey.setApiKey(apiKeyString);
apiKey.setIssuedOn(now);
return getApiKeyObject(apiKey); apiKey.setExpiresOn(expiresOnDate);
} apiKey.setExpiration(expirationInMinutes);
public ApiKeyObject getApiKeyObject(ApiKeyEntity apiKey) throws ApiKeyException { return getApiKeyObject(apiKey);
ZonedDateTime issuedOn = ZonedDateTime.ofInstant(apiKey.getIssuedOn().toInstant(), ZoneId.of("UTC")); }
ZonedDateTime expiresOn = issuedOn.plusMinutes(expirationInMinutes);
String userName = apiKey.getAccount().getUsername(); public ApiKeyObject getApiKeyObject(ApiKeyEntity apiKey) throws ApiKeyException {
try { if (apiKey == null) {
String jwtString = JWTEncoder.encode(password, issuer, issuedOn, userName, apiKey.getApiKey(), apiKey.getExpiration()); throw new ApiKeyException("ApiKey must not be null");
em.persist(apiKey); }
if (LOGGER.isDebugEnabled()) { ZonedDateTime issuedOn = ZonedDateTime.ofInstant(apiKey.getIssuedOn().toInstant(), ZoneId.of("UTC"));
LOGGER.debug("Created API key for {}, valid for {} minutes", userName, expirationInMinutes); ZonedDateTime expiresOn = issuedOn.plusMinutes(expirationInMinutes);
} AccountEntity account = apiKey.getAccount();
if (account == null) {
ApiKeyObject apiKeyObject = new ApiKeyObject(); throw new ApiKeyException("Account of apiKey must not be null");
apiKeyObject.setUserName(userName); }
apiKeyObject.setIssuedOn(Date.from(apiKey.getIssuedOn().toInstant())); String userName = account.getUsername();
apiKeyObject.setExpiresOn(Date.from(expiresOn.toInstant())); try {
apiKeyObject.setAuthToken(jwtString); String jwtString = JWTEncoder.encode(password, issuer, issuedOn, userName, apiKey.getApiKey(), apiKey.getExpiration());
em.persist(apiKey);
return apiKeyObject; if (LOGGER.isDebugEnabled()) {
} catch (JWTException ex) { LOGGER.debug("Created API key for {}, valid for {} minutes", userName, expirationInMinutes);
throw new ApiKeyException("Cannot create apiKey. Reason: " + ex.toString(), ex); }
}
} ApiKeyObject apiKeyObject = new ApiKeyObject();
apiKeyObject.setUserName(userName);
public boolean validateJWT(String encodedJWT) { apiKeyObject.setIssuedOn(Date.from(apiKey.getIssuedOn().toInstant()));
JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT); apiKeyObject.setExpiresOn(Date.from(expiresOn.toInstant()));
ApiKeyEntity validKey; apiKeyObject.setAuthToken(jwtString);
try {
validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); return apiKeyObject;
} catch (JWTException ex) { } catch (JWTException ex) {
if (LOGGER.isTraceEnabled()) { throw new ApiKeyException("Cannot create apiKey. Reason: " + ex.toString(), ex);
LOGGER.trace(ex.toString(), ex); }
} }
return false;
} public boolean validateJWT(String encodedJWT) {
return validKey != null; JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT);
} ApiKeyEntity validKey;
try {
private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) throws JWTException { validKey = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT);
AccountEntity userAccount = accountControl.getAccountEntity(userName, false); } catch (JWTException ex) {
if (userAccount == null) { if (LOGGER.isTraceEnabled()) {
throw new JWTException("AccountControl exception"); LOGGER.trace(ex.toString(), ex);
} }
List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount, true); return false;
if (LOGGER.isTraceEnabled()) { }
LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName); return validKey != null;
} }
Iterator<ApiKeyEntity> it = apiKeys.iterator(); private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) throws JWTException {
ApiKeyEntity keyToLogout = null; AccountEntity userAccount = accountControl.getAccountEntity(userName, false);
while (keyToLogout == null && it.hasNext()) { if (userAccount == null) {
ApiKeyEntity key = it.next(); throw new JWTException("AccountControl exception");
if (key.getApiKey().equals(apiKey)) { }
if (LOGGER.isTraceEnabled()) { List<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount, true);
LOGGER.trace("Found API key in database"); if (LOGGER.isTraceEnabled()) {
} LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName);
}
ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC);
String testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration()); Iterator<ApiKeyEntity> it = apiKeys.iterator();
if (LOGGER.isTraceEnabled()) { ApiKeyEntity keyToLogout = null;
LOGGER.trace("Successfully created validation JWT for user {}", userName); while (keyToLogout == null && it.hasNext()) {
} ApiKeyEntity key = it.next();
if (key.getApiKey().equals(apiKey)) {
if (authorizationHeader.equals(testString)) { if (LOGGER.isTraceEnabled()) {
if (LOGGER.isDebugEnabled()) { LOGGER.trace("Found API key in database");
LOGGER.debug("Found valid key for user {}", userName); }
}
return key; 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 (LOGGER.isDebugEnabled()) { }
LOGGER.debug("No valid key for user {} found", userName);
} if (authorizationHeader.equals(testString)) {
return null; if (LOGGER.isDebugEnabled()) {
} LOGGER.debug("Found valid key for user {}", userName);
}
public String getJWTFromApiKey(ApiKeyEntity apiKey) throws ApiKeyException { return key;
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) { if (LOGGER.isDebugEnabled()) {
throw new ApiKeyException("Cannot retrieve JWT from key. Reason: " + ex.toString(), ex); LOGGER.debug("No valid key for user {} found", userName);
} }
} return null;
}
public JWTObject getJWTObject(String encodedJWT) {
JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT); public String getJWTFromApiKey(ApiKeyEntity apiKey) throws ApiKeyException {
JWTObject jwtObject = new JWTObject(); ZonedDateTime issuedAt = ZonedDateTime.ofInstant(apiKey.getIssuedOn().toInstant(), ZoneOffset.UTC);
jwtObject.setUserName(decoder.getSubject()); try {
jwtObject.setUnqiueId(decoder.getUniqueId()); return JWTEncoder.encode(password, issuer, issuedAt, apiKey.getAccount().getUsername(), apiKey.getApiKey(), apiKey.getExpiration());
jwtObject.setValid(true); } catch (JWTException ex) {
return jwtObject; throw new ApiKeyException("Cannot retrieve JWT from key. Reason: " + ex.toString(), ex);
} }
}
/**
* public JWTObject getJWTObject(String encodedJWT) {
* @param apiKey JWTDecoder decoder = new JWTDecoder(password, issuer, encodedJWT);
* @deprecated use delete (jwtObject) instead JWTObject jwtObject = new JWTObject();
*/ jwtObject.setUserName(decoder.getSubject());
@TransactionAttribute(TransactionAttributeType.REQUIRED) jwtObject.setUnqiueId(decoder.getUniqueId());
@Transactional jwtObject.setValid(true);
@Lock(LockType.WRITE) return jwtObject;
@Deprecated }
public void delete(ApiKeyEntity apiKey) {
em.remove(em.merge(apiKey)); /**
} *
* @param apiKey
@TransactionAttribute(TransactionAttributeType.REQUIRED) * @deprecated use delete (jwtObject) instead
@Transactional */
@Lock(LockType.WRITE) @TransactionAttribute(TransactionAttributeType.REQUIRED)
public void delete(String authorizationHeader) throws ApiKeyException { @Transactional
@Lock(LockType.WRITE)
JWTObject jwtObject = getJWTObject(authorizationHeader); @Deprecated
if (jwtObject.isValid()) { public void delete(ApiKeyEntity apiKey) {
String userName = jwtObject.getUserName(); em.remove(em.merge(apiKey));
}
ApiKeyEntity keyToLogout;
try { @TransactionAttribute(TransactionAttributeType.REQUIRED)
keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader); @Transactional
} catch (JWTException ex) { @Lock(LockType.WRITE)
if (LOGGER.isTraceEnabled()) { public void delete(String authorizationHeader) throws ApiKeyException {
LOGGER.trace(ex.getMessage(), ex);
} JWTObject jwtObject = getJWTObject(authorizationHeader);
if (jwtObject.isValid()) {
keyToLogout = null; String userName = jwtObject.getUserName();
}
ApiKeyEntity keyToLogout;
if (keyToLogout == null) { try {
// no valid key found - must not happen, JWTVeryfingFIlter should have catched this keyToLogout = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader);
// FIXME - add logging / handle this problem } catch (JWTException ex) {
if (LOGGER.isDebugEnabled()) { if (LOGGER.isTraceEnabled()) {
LOGGER.debug("No valid key found, probably user {} has already logged out", userName); LOGGER.trace(ex.getMessage(), ex);
} }
throw new ApiKeyException("No valid key found, probably user " + userName + " has already logged out");
} else { keyToLogout = null;
if (LOGGER.isDebugEnabled()) { }
LOGGER.debug("Found matching apiKey to logout");
} if (keyToLogout == null) {
em.remove(em.merge(keyToLogout)); // no valid key found - must not happen, JWTVeryfingFIlter should have catched this
if (LOGGER.isDebugEnabled()) { // FIXME - add logging / handle this problem
LOGGER.debug("Key deleted, user {} logged out from webservice", userName); if (LOGGER.isDebugEnabled()) {
} LOGGER.debug("No valid key found, probably user {} has already logged out", userName);
} }
} else { throw new ApiKeyException("No valid key found, probably user " + userName + " has already logged out");
throw new ApiKeyException("Provided JWT is not valid"); } 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");
}
}
}