From 7acc23892b1f1cc3d0fe1d7f06fd6624fdbb5921 Mon Sep 17 00:00:00 2001 From: jomu Date: Fri, 14 Dec 2018 09:55:32 +0100 Subject: [PATCH] restructured code and introduced JWTAuthenticationFilter support to ensure user and roles are loaded when using JWT to sign in --- account/pom.xml | 4 + .../account/boundary/AccountProducer.java | 5 +- .../account/boundary/ApiKeyService.java | 395 ++++++++++-------- .../account/control/AccountControl.java | 28 +- .../control/ApplicationController.java | 230 +++++----- .../mail/boundary/MailTemplateService.java | 142 +++---- .../account/shiro/authc/JwtMatcher.java | 109 +++++ .../shiro/filter/JWTAuthenticationFilter.java | 119 ++++++ .../account/shiro/realm/AccountRealm.java | 254 +++++++++++ .../realm}/UserNameActiveDirectoryRealm.java | 2 +- .../shiro/token/JWTAuthenticationToken.java | 50 +++ .../util/AccountSecurityExceptionMapper.java | 22 + .../shared/account/util/SecurityUtil.java | 4 +- .../UserNameActiveDirectoryRealmTest.java | 106 ++--- 14 files changed, 1031 insertions(+), 439 deletions(-) create mode 100644 account/src/main/java/de/muehlencord/shared/account/shiro/authc/JwtMatcher.java create mode 100644 account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java create mode 100644 account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java rename account/src/main/java/de/muehlencord/shared/account/{util => shiro/realm}/UserNameActiveDirectoryRealm.java (99%) create mode 100644 account/src/main/java/de/muehlencord/shared/account/shiro/token/JWTAuthenticationToken.java create mode 100644 account/src/main/java/de/muehlencord/shared/account/util/AccountSecurityExceptionMapper.java rename account/src/test/java/de/muehlencord/shared/account/{util => shiro/realm}/UserNameActiveDirectoryRealmTest.java (94%) diff --git a/account/pom.xml b/account/pom.xml index aca8664..0d93b86 100644 --- a/account/pom.xml +++ b/account/pom.xml @@ -76,6 +76,10 @@ com.google.code.gson gson + + commons-io + commons-io + javax javaee-api diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/AccountProducer.java b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/AccountProducer.java index 97b4e54..95d6502 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/AccountProducer.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/AccountProducer.java @@ -77,13 +77,12 @@ public class AccountProducer implements Serializable { } if ((subject.isAuthenticated() == false) && (subject.isRemembered() == false)) { - accountName = "web"; + return null; } else { accountName = subject.getPrincipal().toString(); } account = accountController.getAccountEntity(accountName, true); - // TODO introduce locale support to account and switch - // to pre-defined locale if set + // TODO introduce locale support to account and switch to pre-defined locale if set } return account; } 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 791c253..c800569 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,179 +1,216 @@ -package de.muehlencord.shared.account.business.account.boundary; - -import de.muehlencord.shared.account.business.config.boundary.ConfigService; -import de.muehlencord.shared.account.business.account.control.AccountControl; -import de.muehlencord.shared.account.business.account.entity.Account; -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.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 java.io.Serializable; -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; - - @Inject - Account account; - - 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 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() { - return getUsersApiKeys(accountControl.getAccountEntity(account.getUsername(), false)); - } - - @Transactional - @Lock(LockType.WRITE) - public String createNewApiKey(ZonedDateTime now, short expirationInMinutes) throws ApiKeyException { - if ((password == null || issuer == null)) { - LOGGER.error("password or issuer not set in, please validate configuration"); - } - Date nowDate = Date.from(now.toInstant()); - String apiKeyString = RandomStringUtils.randomAscii(50); - - ApiKeyEntity apiKey = new ApiKeyEntity(); - apiKey.setAccount(accountControl.getAccountEntity(account.getUsername(), false)); - apiKey.setApiKey(apiKeyString); - apiKey.setIssuedOn(nowDate); - apiKey.setExpiration(expirationInMinutes); - - try { - String jwtString = JWTEncoder.encode(password, issuer, now, 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 = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); - return validKey != null; - } - - public ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) { - AccountEntity userAccount = accountControl.getAccountEntity(userName, false); - List apiKeys = getUsersApiKeys(userAccount); - - Iterator it = apiKeys.iterator(); - ApiKeyEntity keyToLogout = null; - while (keyToLogout == null && it.hasNext()) { - ApiKeyEntity key = it.next(); - if (key.getApiKey().equals(apiKey)) { - ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC); - String testString; - - try { - testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration()); - if (authorizationHeader.equals(testString)) { - return key; - } - } catch (JWTException ex) { - - } - } - } - 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 authorizationHeader) { - JWTDecoder decoder = new JWTDecoder(password, issuer, authorizationHeader); - JWTObject jwtObject = new JWTObject(); - jwtObject.setUserName(decoder.getSubject()); - jwtObject.setUnqiueId(decoder.getUniqueId()); - jwtObject.setValid(true); - return jwtObject; - } - - @TransactionAttribute(TransactionAttributeType.REQUIRED) - @Transactional - @Lock(LockType.WRITE) - public void delete(ApiKeyEntity apiKey) { - em.remove(em.merge(apiKey)); - } - -} +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.Account; +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 java.io.Serializable; +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; + + @Inject + Account account; + + 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 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() { + return getUsersApiKeys(accountControl.getAccountEntity(account.getUsername(), false)); + } + + @Transactional + @Lock(LockType.WRITE) + public String createNewApiKey(ZonedDateTime now, short expirationInMinutes) throws ApiKeyException { + if ((password == null || issuer == null)) { + LOGGER.error("password or issuer not set in, please validate configuration"); + } + Date nowDate = Date.from(now.toInstant()); + String apiKeyString = RandomStringUtils.randomAscii(50); + + ApiKeyEntity apiKey = new ApiKeyEntity(); + apiKey.setAccount(accountControl.getAccountEntity(account.getUsername(), false)); + apiKey.setApiKey(apiKeyString); + apiKey.setIssuedOn(nowDate); + apiKey.setExpiration(expirationInMinutes); + + try { + String jwtString = JWTEncoder.encode(password, issuer, now, 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 = getValidKey(decoder.getSubject(), decoder.getUniqueId(), encodedJWT); + return validKey != null; + } + + private ApiKeyEntity getValidKey(String userName, String apiKey, String authorizationHeader) { + AccountEntity userAccount = accountControl.getAccountEntity(userName, false); + List apiKeys = getUsersApiKeys(userAccount); + + Iterator it = apiKeys.iterator(); + ApiKeyEntity keyToLogout = null; + while (keyToLogout == null && it.hasNext()) { + ApiKeyEntity key = it.next(); + if (key.getApiKey().equals(apiKey)) { + ZonedDateTime issuedOn = ZonedDateTime.ofInstant(key.getIssuedOn().toInstant(), ZoneOffset.UTC); + String testString; + + try { + testString = JWTEncoder.encode(password, issuer, issuedOn, key.getAccount().getUsername(), key.getApiKey(), key.getExpiration()); + if (authorizationHeader.equals(testString)) { + return key; + } + } catch (JWTException ex) { + + } + } + } + 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 = getValidKey(userName, jwtObject.getUnqiueId(), authorizationHeader); + + 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"); + } + } +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java b/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java index 2581371..7ae4d08 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java @@ -1,13 +1,13 @@ package de.muehlencord.shared.account.business.account.control; -import de.muehlencord.shared.account.business.account.entity.AccountException; -import de.muehlencord.shared.account.business.account.entity.AccountStatus; -import de.muehlencord.shared.account.business.mail.entity.MailException; -import de.muehlencord.shared.account.business.mail.boundary.MailService; import de.muehlencord.shared.account.business.account.entity.AccountEntity; +import de.muehlencord.shared.account.business.account.entity.AccountException; import de.muehlencord.shared.account.business.account.entity.AccountLoginEntity; -import de.muehlencord.shared.account.business.application.entity.ApplicationRoleEntity; +import de.muehlencord.shared.account.business.account.entity.AccountStatus; import de.muehlencord.shared.account.business.application.entity.ApplicationEntity; +import de.muehlencord.shared.account.business.application.entity.ApplicationRoleEntity; +import de.muehlencord.shared.account.business.mail.boundary.MailService; +import de.muehlencord.shared.account.business.mail.entity.MailException; import de.muehlencord.shared.account.util.AccountPU; import de.muehlencord.shared.account.util.SecurityUtil; import de.muehlencord.shared.util.DateUtil; @@ -23,10 +23,10 @@ import javax.persistence.NoResultException; import javax.persistence.Query; import javax.transaction.Transactional; import org.apache.commons.lang3.RandomStringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -47,7 +47,7 @@ public class AccountControl implements Serializable { @Inject @AccountPU EntityManager em; - + /** * returns a list of active accounts * @@ -156,7 +156,7 @@ public class AccountControl implements Serializable { @Transactional public void deleteAccount(AccountEntity account) throws AccountException { - Date now = new Date(); // Todo now in UTC + Date now = DateUtil.getCurrentTimeInUTC(); Subject currentUser = SecurityUtils.getSubject(); String currentUserName = currentUser.getPrincipal().toString(); @@ -186,7 +186,7 @@ public class AccountControl implements Serializable { String randomString = RandomStringUtils.random(40, true, true); - Date validTo = new Date(); // TODO now in UTC + Date validTo = DateUtil.getCurrentTimeInUTC(); validTo = new Date(validTo.getTime() + 1000 * 600); // 10 minutes to react // TODO rework password reset @@ -217,7 +217,7 @@ public class AccountControl implements Serializable { /* if (account.getPasswordResetOngoing() && (account.getPasswordResetHash() != null) && (account.getPasswordResetValidTo() != null)) { - Date now = new Date(); // TODO now in UTC + Date now = DateUtil.getCurrentTimeInUTC(); String storedHash = account.getPasswordResetHash().trim(); if (account.getPasswordResetValidTo().after(now)) { if (storedHash.equals(resetPasswordToken)) { @@ -248,7 +248,7 @@ public class AccountControl implements Serializable { } private void executePasswordReset(AccountEntity account, String newPassword) { - Date now = new Date(); // TODO now in UTC + Date now = DateUtil.getCurrentTimeInUTC(); String hashedPassword = SecurityUtil.createPassword(newPassword); // account.setAccountPassword(hashedPassword); @@ -294,7 +294,7 @@ public class AccountControl implements Serializable { public void addLoginError(AccountEntity account) { // TODO reimplement // try { -// Date now = new Date(); // TODO now in UTC +// Date now = DateUtil.getCurrentTimeInUTC(); // account.setLastFailedLogin(now); // account.setFailureCount(account.getFailureCount() + 1); // @@ -367,5 +367,5 @@ public class AccountControl implements Serializable { em.remove(login); em.merge(account); } - + } diff --git a/account/src/main/java/de/muehlencord/shared/account/business/application/control/ApplicationController.java b/account/src/main/java/de/muehlencord/shared/account/business/application/control/ApplicationController.java index 512effc..d2b1856 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/application/control/ApplicationController.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/application/control/ApplicationController.java @@ -1,116 +1,114 @@ -/* - * 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.application.control; - -import de.muehlencord.shared.account.business.application.boundary.ApplicationService; -import de.muehlencord.shared.account.business.application.entity.ApplicationEntity; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; -import java.util.UUID; -import javax.annotation.PostConstruct; -import javax.ejb.EJB; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.Produces; -import javax.inject.Named; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author Joern Muehlencord - */ -@Named("applicationController") -@ApplicationScoped -public class ApplicationController { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationController.class); - - @EJB - ApplicationService applicationService; - - private String version; - private String buildDate; - private UUID uuid; - private ApplicationEntity application = null; - - @PostConstruct - public void readBuildInfoProperties() { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Trying to read buildInfo.properties"); - } - InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("buildInfo.properties"); - if (in == null) { - return; - } - Properties props = new Properties(); - try { - props.load(in); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("properties read from buildInfo.properties"); - } - - version = props.getProperty("build.version"); - buildDate = props.getProperty("build.timestamp"); - uuid = UUID.fromString(props.getProperty("application.uuid")); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("buildInfo.properties parsed successfully"); - } - - } catch (IOException ex) { - LOGGER.error("Cannot find buildInfo.properties. ", ex); - version = "??"; - buildDate = "??"; - uuid = null; - LOGGER.error("Application id not readable, application will not be able to run"); - } - - if (uuid != null) { - this.application = applicationService.findById(uuid); - if (application == null) { - LOGGER.error("Could not find application with id "); - } else { - LOGGER.info("Found application {} with id {}", application.getApplicationName(), uuid.toString()); - } - } - } - - /** - * needs to return link to "Account UI" and not to current selected - * application TODO: ensure only Account UI can call functions where - * appliction can be handed in - all other applications need to call the - * function which use the injected application - */ - @Produces - public ApplicationEntity getApplication() { - return application; - - } - - public String getVersion() { - return version; - } - - public String getBuildDate() { - return buildDate; - } - - public UUID getApplicationId() { - return uuid; - } - -} +/* + * 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.application.control; + +import de.muehlencord.shared.account.business.application.boundary.ApplicationService; +import de.muehlencord.shared.account.business.application.entity.ApplicationEntity; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.UUID; +import javax.annotation.PostConstruct; +import javax.ejb.EJB; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Named; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Joern Muehlencord + */ +@Named("applicationController") +@ApplicationScoped +public class ApplicationController { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationController.class); + + @EJB + ApplicationService applicationService; + + private String version; + private String buildDate; + private UUID uuid; + private ApplicationEntity application = null; + + @PostConstruct + public void readBuildInfoProperties() { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Trying to read buildInfo.properties"); + } + InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("buildInfo.properties"); + if (in == null) { + return; + } + Properties props = new Properties(); + try { + props.load(in); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("properties read from buildInfo.properties"); + } + + version = props.getProperty("build.version"); + buildDate = props.getProperty("build.timestamp"); + uuid = UUID.fromString(props.getProperty("application.uuid")); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("buildInfo.properties parsed successfully"); + } + + } catch (IOException ex) { + LOGGER.error("Cannot find buildInfo.properties. ", ex); + version = "??"; + buildDate = "??"; + uuid = null; + LOGGER.error("Application id not readable, application will not be able to run"); + } + + if (uuid != null) { + this.application = applicationService.findById(uuid); + if (application == null) { + LOGGER.error("Could not find application with id "); + } else { + LOGGER.info("Found application {} with id {}", application.getApplicationName(), uuid.toString()); + } + } + } + + /** + * needs to return link to "Account UI" and not to current selected application + * TODO: ensure only Account UI can call functions where appliction can be handed in - all other applications need to call the function which use the injected application + */ + @Produces + public ApplicationEntity getApplication() { + return application; + + } + + public String getVersion() { + return version; + } + + public String getBuildDate() { + return buildDate; + } + + public UUID getApplicationId() { + return uuid; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/mail/boundary/MailTemplateService.java b/account/src/main/java/de/muehlencord/shared/account/business/mail/boundary/MailTemplateService.java index 49b6002..891cb63 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/mail/boundary/MailTemplateService.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/mail/boundary/MailTemplateService.java @@ -1,71 +1,71 @@ -package de.muehlencord.shared.account.business.mail.boundary; - -import de.muehlencord.shared.account.business.mail.entity.MailDatamodel; -import de.muehlencord.shared.account.business.mail.entity.MailTemplateException; -import de.muehlencord.shared.account.business.mail.entity.MailTemplateEntity; -import de.muehlencord.shared.account.util.AccountPU; -import freemarker.cache.StringTemplateLoader; -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateExceptionHandler; -import java.io.Serializable; -import java.io.StringWriter; -import java.io.Writer; -import javax.ejb.Stateless; -import javax.inject.Inject; -import javax.persistence.EntityManager; -import javax.persistence.Query; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author jomu - */ -@Stateless -public class MailTemplateService implements Serializable { - - private static final long serialVersionUID = -136113381443058697L; - - private static final Logger LOGGER = LoggerFactory.getLogger(MailTemplateService.class.getName()); - - @Inject - @AccountPU - EntityManager em; - - public String getStringFromTemplate(String templateName, MailDatamodel dataModel) throws MailTemplateException { - try { - Query query = em.createNamedQuery("MailTemplateEntity.findByTemplateName"); - query.setParameter("templateName", templateName); - MailTemplateEntity templateEntity = (MailTemplateEntity) query.getSingleResult(); - if (templateEntity == null) { - LOGGER.error("Tempate with name " + templateName + " not found"); - return null; - } - - Configuration configuration = new Configuration(Configuration.VERSION_2_3_23); - configuration.setDefaultEncoding("UTF-8"); // FIXME make encoding configurable - configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - - StringTemplateLoader stringLoader = new StringTemplateLoader(); - stringLoader.putTemplate(templateEntity.getTemplateName(), templateEntity.getTemplateValue()); - configuration.setTemplateLoader(stringLoader); - - Template template = configuration.getTemplate(templateEntity.getTemplateName()); - - Writer out = new StringWriter(); - template.process(dataModel, out); - String templateString = out.toString(); - return templateString; - } catch (Exception ex) { - String hint = "Error while processing template with name " + templateName + "."; - LOGGER.error(hint + " " + ex.toString()); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(hint, ex); - } - throw new MailTemplateException(hint, ex); - - } - - } -} +package de.muehlencord.shared.account.business.mail.boundary; + +import de.muehlencord.shared.account.business.mail.entity.MailDatamodel; +import de.muehlencord.shared.account.business.mail.entity.MailTemplateEntity; +import de.muehlencord.shared.account.business.mail.entity.MailTemplateException; +import de.muehlencord.shared.account.util.AccountPU; +import freemarker.cache.StringTemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author jomu + */ +@Stateless +public class MailTemplateService implements Serializable { + + private static final long serialVersionUID = -136113381443058697L; + + private static final Logger LOGGER = LoggerFactory.getLogger(MailTemplateService.class.getName()); + + @Inject + @AccountPU + EntityManager em; + + public String getStringFromTemplate(String templateName, MailDatamodel dataModel) throws MailTemplateException { + try { + Query query = em.createNamedQuery("MailTemplateEntity.findByTemplateName"); + query.setParameter("templateName", templateName); + MailTemplateEntity templateEntity = (MailTemplateEntity) query.getSingleResult(); + if (templateEntity == null) { + LOGGER.error("Tempate with name " + templateName + " not found"); + return null; + } + + Configuration configuration = new Configuration(Configuration.VERSION_2_3_23); + configuration.setDefaultEncoding("UTF-8"); // FIXME - make encoding configurable + configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + StringTemplateLoader stringLoader = new StringTemplateLoader(); + stringLoader.putTemplate(templateEntity.getTemplateName(), templateEntity.getTemplateValue()); + configuration.setTemplateLoader(stringLoader); + + Template template = configuration.getTemplate(templateEntity.getTemplateName()); + + Writer out = new StringWriter(); + template.process(dataModel, out); + String templateString = out.toString(); + return templateString; + } catch (Exception ex) { + String hint = "Error while processing template with name " + templateName + "."; + LOGGER.error(hint + " " + ex.toString()); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(hint, ex); + } + throw new MailTemplateException(hint, ex); + + } + + } +} diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/authc/JwtMatcher.java b/account/src/main/java/de/muehlencord/shared/account/shiro/authc/JwtMatcher.java new file mode 100644 index 0000000..ffbb432 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/authc/JwtMatcher.java @@ -0,0 +1,109 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package de.muehlencord.shared.account.shiro.authc; + +import de.muehlencord.shared.account.business.account.boundary.ApiKeyService; +import de.muehlencord.shared.account.business.account.entity.JWTObject; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author joern.muehlencord + */ +public class JwtMatcher implements CredentialsMatcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(JwtMatcher.class); + private final ApiKeyService apiKeyService = lookupApiKeyServiceBean(); + + @Override + public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { + if (token == null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No token available - cannot match credentials"); + } + return false; + } + + if ((info == null) || (info.getCredentials() == null)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No authenticationInfo available - cannot match credentials"); + } + return false; + } + + Object submittedJwtObj = token.getCredentials(); + Object storedCredentials = getStoredPassword(info); + if ((submittedJwtObj != null) && (submittedJwtObj.getClass().isAssignableFrom(String.class))) { + String submittedJwt = (String) submittedJwtObj; + if (apiKeyService.validateJWT(submittedJwt)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JWT is valid, checking if it comes from the correct user"); + } + + JWTObject jwtObject = apiKeyService.getJWTObject(submittedJwt); + String storedUsername = info.getPrincipals().getPrimaryPrincipal().toString(); + if (jwtObject.getUserName().equals(storedUsername)) { + if (jwtObject.getUnqiueId().equals (storedCredentials)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JWT matches user and password is correct"); + } + return true; + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JWT password does not match provided password"); + } + return false; + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JWT belongs to user {}, but authinfo is from user {} - JWT does not match"); + } + return false; + } + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JWT is invalid"); + } + return false; + } + } else { + LOGGER.error("Unexpected authInfoFormat - please check your configuration"); + return false; + } + } + + protected Object getStoredPassword(AuthenticationInfo storedAccountInfo) { + Object stored = storedAccountInfo != null ? storedAccountInfo.getCredentials() : null; + //fix for https://issues.apache.org/jira/browse/SHIRO-363 + if (stored instanceof char[]) { + stored = new String((char[]) stored); + } + return stored; + } + + // TODO - can this be injected? + private ApiKeyService lookupApiKeyServiceBean() { + try { + Context c = new InitialContext(); + return (ApiKeyService) c.lookup("java:module/ApiKeyService"); // NOI18N + } catch (NamingException ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(ex.toString(), ex); + } else { + LOGGER.error(ex.toString()); + } + throw new RuntimeException(ex); + } + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java new file mode 100644 index 0000000..28a5de7 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java @@ -0,0 +1,119 @@ +package de.muehlencord.shared.account.shiro.filter; + +import de.muehlencord.shared.account.business.account.boundary.ApiKeyService; +import de.muehlencord.shared.account.business.account.entity.JWTObject; +import de.muehlencord.shared.account.shiro.token.JWTAuthenticationToken; +import java.io.StringReader; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.apache.shiro.web.util.WebUtils; +import org.slf4j.LoggerFactory; + +/** + * + * @author Joern Muehlencord + */ +public final class JWTAuthenticationFilter extends AuthenticatingFilter { + + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(JWTAuthenticationFilter.class); + protected static final String AUTHORIZATION_HEADER = "Authorization"; // NOI18N + + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + private final ApiKeyService apiKeyService = lookupApiKeyServiceBean(); + + public JWTAuthenticationFilter() { + // FIXME - logging in via JWTAuthenticationFilter does not yet work. Need to set login to anonymous to execute login in rest service + setLoginUrl("/rest/account/login"); + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + boolean loggedIn = false; + + if (isLoginRequest(request, response) || isLoggedAttempt(request, response)) { + loggedIn = executeLogin(request, response); + } + + if (!loggedIn) { + HttpServletResponse httpResponse = WebUtils.toHttp(response); + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + return loggedIn; + } + + @Override + protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { + if (isLoginRequest(request, response)) { + String json = IOUtils.toString(request.getInputStream(), "UTF-8"); // FIXME - check charset in request + + if (json != null && !json.isEmpty()) { + try (JsonReader jr = Json.createReader(new StringReader(json))) { + JsonObject object = jr.readObject(); + String username = object.getString(USERNAME); + String password = object.getString(PASSWORD); + return new UsernamePasswordToken(username, password); + } + + } + } + + if (isLoggedAttempt(request, response)) { + String jwtToken = getAuthzHeader(request); + if (jwtToken != null) { + return createToken(jwtToken); + } + } + + return new UsernamePasswordToken(); + } + + protected boolean isLoggedAttempt(ServletRequest request, ServletResponse response) { + String authzHeader = getAuthzHeader(request); + return authzHeader != null; + } + + protected String getAuthzHeader(ServletRequest request) { + HttpServletRequest httpRequest = WebUtils.toHttp(request); + return httpRequest.getHeader(AUTHORIZATION_HEADER); + } + + public JWTAuthenticationToken createToken(String token) { + if (apiKeyService.validateJWT(token)) { + JWTObject jwtObject = apiKeyService.getJWTObject(token); + return new JWTAuthenticationToken(jwtObject.getUserName(), token); + } else { + throw new AuthenticationException("provided API key invalid"); + } + } + + // TODO - can this be injected? + private ApiKeyService lookupApiKeyServiceBean() { + try { + Context c = new InitialContext(); + return (ApiKeyService) c.lookup("java:module/ApiKeyService"); // NOI18N + } catch (NamingException ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(ex.toString(), ex); + } else { + LOGGER.error(ex.toString()); + } + throw new RuntimeException(ex); + } + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java new file mode 100644 index 0000000..40bdb85 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java @@ -0,0 +1,254 @@ +package de.muehlencord.shared.account.shiro.realm; + +import de.muehlencord.shared.account.shiro.authc.JwtMatcher; +import de.muehlencord.shared.account.shiro.token.JWTAuthenticationToken; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import org.apache.shiro.authc.AccountException; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.IncorrectCredentialsException; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; +import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.realm.jdbc.JdbcRealm; +import org.apache.shiro.util.JdbcUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author joern.muehlencord + */ +public class AccountRealm extends JdbcRealm { + + private static final Logger LOGGER = LoggerFactory.getLogger(AccountRealm.class); + + protected String applicationId = null; + protected String jwtAuthenticationQuery = "select ak.api_key from account a, api_key ak where ak.account = a.id and a.username = ? and a.status not in ('LOCKED','DELETED','DISABLED') ORDER BY ak.issued_on ASC"; + protected CredentialsMatcher jwtMatcher = new JwtMatcher(); + + public AccountRealm() { + this.authenticationQuery = "select al.account_password from account a, account_login al where al.account = a.id and a.username = ? and status not in ('LOCKED','DELETED','DISABLED')"; + this.userRolesQuery = "select r.role_name from application_role r, account_role ar, account a WHERE a.username = ? AND a.id = ar.account AND ar.account_role = r.id"; + this.permissionsQuery = "select permission_name from application_role appr, role_permission rp, application_permission appp WHERE appr.role_name = ? AND appr.application = ? AND rp.application_role = appr.id AND rp.role_permission = appp.id"; + this.permissionsLookupEnabled = true; + } + + @Override + public boolean supports(AuthenticationToken token) { + super.supports(token); + return token != null && (token instanceof JWTAuthenticationToken || token instanceof UsernamePasswordToken); + } + + public boolean isJwtAuthentication(AuthenticationToken token) { + if (token == null) { + throw new AuthenticationException("empty tokens are not supported by this realm"); + } + + if (token.getClass().isAssignableFrom(JWTAuthenticationToken.class)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Handling JWTAuthenticationToken"); + } + return true; + } else if (token.getClass().isAssignableFrom(UsernamePasswordToken.class)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Handling UsernamePasswordToken"); + } + return false; + } else { + throw new AuthenticationException("Handling of " + token.getClass() + " not supported by this realm"); + } + } + + /** + * overwritten getPermissions. Only change is to inject the applicationId + * into the the query + * + * @param conn the connection to use + * @param username the user to lookup + * @param roleNames the users roles + * @return a list of permissions + * @throws SQLException if the SQL query fails + */ + @Override + protected Set getPermissions(Connection conn, String username, Collection roleNames) throws SQLException { + PreparedStatement ps = null; + Set permissions = new LinkedHashSet<>(); + try { + ps = conn.prepareStatement(permissionsQuery); + for (String roleName : roleNames) { + + ps.setString(1, roleName); + ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm + + ResultSet rs = null; + + try { + // Execute query + rs = ps.executeQuery(); + + // Loop over results and add each returned role to a set + while (rs.next()) { + + String permissionString = rs.getString(1); + + // Add the permission to the set of permissions + permissions.add(permissionString); + } + } finally { + JdbcUtils.closeResultSet(rs); + } + + } + } finally { + JdbcUtils.closeStatement(ps); + } + + return permissions; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + if (isJwtAuthentication(token)) { + return doGetJwtAuthenticationInfo(token); + } else { + return super.doGetAuthenticationInfo(token); + } + } + + protected AuthenticationInfo doGetJwtAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + JWTAuthenticationToken jwtToken = (JWTAuthenticationToken) token; + String username = jwtToken.getUserName(); + + // Null username is invalid + if (username == null) { + throw new AccountException("Null usernames are not allowed by this realm."); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Checking JWT for user {}", username); + } + + Connection conn = null; + SimpleAuthenticationInfo info = null; + try { + conn = dataSource.getConnection(); + + String apiKey = getApiKeyForJwtUser(conn, username); + + if (apiKey == null) { + throw new UnknownAccountException("No api key found for user [" + username + "]"); + } + + info = new SimpleAuthenticationInfo(username, apiKey.toCharArray(), getName()); + + } catch (SQLException ex) { + final String message = "There was a SQL error while authenticating user [" + username + "]"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(message, ex); + } else { + LOGGER.error(ex.toString()); + } + // Rethrow any SQL errors as an authentication exception + throw new AuthenticationException(message, ex); + } finally { + JdbcUtils.closeConnection(conn); + } + + return info; + } + + private String getApiKeyForJwtUser(Connection conn, String username) throws SQLException { + + String result = null; + + PreparedStatement ps = null; + ResultSet rs = null; + try { + ps = conn.prepareStatement(jwtAuthenticationQuery); + ps.setString(1, username); + + // Execute query + rs = ps.executeQuery(); + // loop through result, take last one (by default ordered by issue date ASC) + // we only expect one - application should delete all obsolete ones automatically + while (rs.next()) { + result = rs.getString(1); + } + } finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(ps); + } + + return result; + } + + /** + * Asserts that the submitted {@code AuthenticationToken}'s credentials + * match the stored account {@code AuthenticationInfo}'s credentials, and if + * not, throws an {@link AuthenticationException}. + * + * @param token the submitted authentication token + * @param info the AuthenticationInfo corresponding to the given + * {@code token} + * @throws AuthenticationException if the token's credentials do not match + * the stored account credentials. + */ + @Override + protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { + CredentialsMatcher cm; + if (isJwtAuthentication(token)) { + cm = getJwtMatcher(); + } else { + cm = getCredentialsMatcher(); + } + + if (cm != null) { + if (!cm.doCredentialsMatch(token, info)) { + //not successful - throw an exception to indicate this: + String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; + throw new IncorrectCredentialsException(msg); + } + } else { + throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + + "credentials during authentication. If you do not wish for credentials to be examined, you " + + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); + } + } + + /* *** getter / setter *** */ + public String getApplicationId() { + return this.applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public String getJwtAuthenticationQuery() { + return jwtAuthenticationQuery; + } + + public void setJwtAuthenticationQuery(String jwtAuthenticationQuery) { + this.jwtAuthenticationQuery = jwtAuthenticationQuery; + } + + public CredentialsMatcher getJwtMatcher() { + return jwtMatcher; + } + + public void setJwtMatcher(CredentialsMatcher jwtMatcher) { + this.jwtMatcher = jwtMatcher; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealm.java b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java similarity index 99% rename from account/src/main/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealm.java rename to account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java index 3f8a4d1..f2f825e 100644 --- a/account/src/main/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealm.java +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java @@ -1,4 +1,4 @@ -package de.muehlencord.shared.account.util; +package de.muehlencord.shared.account.shiro.realm; import java.util.HashSet; import java.util.Set; diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/token/JWTAuthenticationToken.java b/account/src/main/java/de/muehlencord/shared/account/shiro/token/JWTAuthenticationToken.java new file mode 100644 index 0000000..60faed7 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/token/JWTAuthenticationToken.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package de.muehlencord.shared.account.shiro.token; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * + * @author joern.muehlencord + */ +public class JWTAuthenticationToken implements AuthenticationToken { + + private String userName; + private String token; + + public JWTAuthenticationToken(String userId, String token) { + this.userName = userId; + this.token = token; + } + + @Override + public String getPrincipal() { + return getUserName(); + } + + @Override + public String getCredentials() { + return getToken(); + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userId) { + this.userName = userId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/util/AccountSecurityExceptionMapper.java b/account/src/main/java/de/muehlencord/shared/account/util/AccountSecurityExceptionMapper.java new file mode 100644 index 0000000..37467b3 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/util/AccountSecurityExceptionMapper.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package de.muehlencord.shared.account.util; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +/** + * + * @author joern.muehlencord + */ +public class AccountSecurityExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AccountSecurityException ex) { + return Response.status(Response.Status.UNAUTHORIZED).entity(ex.getMessage()).build(); + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/util/SecurityUtil.java b/account/src/main/java/de/muehlencord/shared/account/util/SecurityUtil.java index 97a2f73..5c9ec56 100644 --- a/account/src/main/java/de/muehlencord/shared/account/util/SecurityUtil.java +++ b/account/src/main/java/de/muehlencord/shared/account/util/SecurityUtil.java @@ -1,12 +1,12 @@ package de.muehlencord.shared.account.util; import org.apache.shiro.SecurityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.shiro.authc.credential.DefaultPasswordService; import org.apache.shiro.crypto.hash.DefaultHashService; import org.apache.shiro.crypto.hash.Sha512Hash; import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * diff --git a/account/src/test/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealmTest.java b/account/src/test/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealmTest.java similarity index 94% rename from account/src/test/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealmTest.java rename to account/src/test/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealmTest.java index 8f84201..3a8064b 100644 --- a/account/src/test/java/de/muehlencord/shared/account/util/UserNameActiveDirectoryRealmTest.java +++ b/account/src/test/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealmTest.java @@ -1,53 +1,53 @@ -package de.muehlencord.shared.account.util; - -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authc.AuthenticationException; -import org.apache.shiro.authc.UsernamePasswordToken; -import org.apache.shiro.config.IniSecurityManagerFactory; -import org.apache.shiro.subject.Subject; -import org.junit.Test; -import org.apache.shiro.mgt.SecurityManager; -import static org.junit.Assume.assumeNotNull; - -/** - * - * @author Joern Muehlencord - */ -public class UserNameActiveDirectoryRealmTest { - - @Test - public void testUsernameLogin() { - String userName = "user.name"; - String password = "secret"; - testLogin(userName, password); - } - - @Test - public void testEmailaddressLogin() { - String userName = "user.name@domain.com"; - String password = "secret"; - testLogin(userName, password); - } - - @Test(expected=AuthenticationException.class) - public void testWrongUserNamePassword() { - String userName = "test123"; - String password = "secret"; - testLogin(userName, password); - } - - private void testLogin(String userName, String password) throws AuthenticationException { - assumeNotNull(UserNameActiveDirectoryRealmTest.class.getResource("/shiro.ini")); - - IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); - SecurityManager securityManager = factory.getInstance(); - SecurityUtils.setSecurityManager(securityManager); - - UsernamePasswordToken token = new UsernamePasswordToken(userName, password); - Subject currentUser = SecurityUtils.getSubject(); - - currentUser.login(token); - System.out.println("Logged in"); - } - -} +package de.muehlencord.shared.account.shiro.realm; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.config.IniSecurityManagerFactory; +import org.apache.shiro.subject.Subject; +import org.junit.Test; +import org.apache.shiro.mgt.SecurityManager; +import static org.junit.Assume.assumeNotNull; + +/** + * + * @author Joern Muehlencord + */ +public class UserNameActiveDirectoryRealmTest { + + @Test + public void testUsernameLogin() { + String userName = "user.name"; + String password = "secret"; + testLogin(userName, password); + } + + @Test + public void testEmailaddressLogin() { + String userName = "user.name@domain.com"; + String password = "secret"; + testLogin(userName, password); + } + + @Test(expected=AuthenticationException.class) + public void testWrongUserNamePassword() { + String userName = "test123"; + String password = "secret"; + testLogin(userName, password); + } + + private void testLogin(String userName, String password) throws AuthenticationException { + assumeNotNull(UserNameActiveDirectoryRealmTest.class.getResource("/shiro.ini")); + + IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); + SecurityManager securityManager = factory.getInstance(); + SecurityUtils.setSecurityManager(securityManager); + + UsernamePasswordToken token = new UsernamePasswordToken(userName, password); + Subject currentUser = SecurityUtils.getSubject(); + + currentUser.login(token); + System.out.println("Logged in"); + } + +}