From 7b315f6fd06532af296438b2d0f3005327ca72e4 Mon Sep 17 00:00:00 2001 From: jomu Date: Mon, 18 Feb 2019 22:17:52 +0100 Subject: [PATCH] made JTW expiration configurable --- .../account/boundary/ApiKeyService.java | 573 +++++++++--------- 1 file changed, 297 insertions(+), 276 deletions(-) diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java index 6acbf3a..52de1ec 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java @@ -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 - */ -@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 apiKeys = query.getResultList(); - if ((apiKeys == null) || (apiKeys.isEmpty())) { - throw new ApiKeyException("ApiKey not found in database"); - } - return apiKeys.get(0); - } - - public List getUsersApiKeys(AccountEntity account) { - Query query = em.createNamedQuery("ApiKeyEntity.findByAccount"); - query.setParameter("account", account); - List keys = query.getResultList(); - if (keys == null) { - return new ArrayList<>(); - } else { - return keys; - } - - } - - public List 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 apiKeys = getUsersApiKeys(userAccount); - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName); - } - - Iterator 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 + */ +@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 apiKeys = query.getResultList(); + if ((apiKeys == null) || (apiKeys.isEmpty())) { + throw new ApiKeyException("ApiKey not found in database"); + } + return apiKeys.get(0); + } + + public List getUsersApiKeys(AccountEntity account) { + Query query = em.createNamedQuery("ApiKeyEntity.findByAccount"); + query.setParameter("account", account); + List keys = query.getResultList(); + if (keys == null) { + return new ArrayList<>(); + } else { + return keys; + } + + } + + public List 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 apiKeys = getUsersApiKeys(userAccount); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Found {} keys for user {}", apiKeys.size(), userName); + } + + Iterator 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"); + } + } +}