diff --git a/account/pom.xml b/account/pom.xml index 9ced575..39de8a5 100644 --- a/account/pom.xml +++ b/account/pom.xml @@ -72,6 +72,10 @@ 1.1-SNAPSHOT jar + + com.google.code.gson + gson + javax javaee-api diff --git a/account/sql/account.dbm b/account/sql/account.dbm index 3d64657..976ceaa 100644 --- a/account/sql/account.dbm +++ b/account/sql/account.dbm @@ -3,7 +3,7 @@ CAUTION: Do not modify this file unless you know what you are doing. Unexpected results may occur if the code is changed deliberately. --> - @@ -13,7 +13,7 @@ CAUTION: Do not modify this file unless you know what you are doing. - + @@ -36,7 +36,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -59,7 +59,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -100,7 +100,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -129,7 +129,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -143,7 +143,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -166,7 +166,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -180,7 +180,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -194,7 +194,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -212,7 +212,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
- + @@ -260,6 +260,29 @@ CAUTION: Do not modify this file unless you know what you are doing.
+ + + + + + + + + + + + + + + + + + + + + +
+ @@ -320,6 +343,12 @@ CAUTION: Do not modify this file unless you know what you are doing. + + + + + + +
diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyException.java b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyException.java new file mode 100644 index 0000000..51cbafc --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyException.java @@ -0,0 +1,40 @@ +/* + * 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.business.account.boundary; + +/** + * + * @author Joern Muehlencord + */ +public class ApiKeyException extends Exception { + + private static final long serialVersionUID = -8017749942111814929L; + + /** + * Creates a new instance of ApiKeyException without detail message. + */ + public ApiKeyException() { + } + + + /** + * Constructs an instance of ApiKeyException with the specified detail message. + * @param msg the detail message. + */ + public ApiKeyException(String msg) { + super(msg); + } + + /** + * Constructs an instance of ApiKeyException with the specified detail message and root cause. + * @param msg the detail message. + * @param th the root cause + */ + public ApiKeyException(String msg, Throwable th) { + super(msg,th); + } +} 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 new file mode 100644 index 0000000..791c253 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/boundary/ApiKeyService.java @@ -0,0 +1,179 @@ +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)); + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java new file mode 100644 index 0000000..22b89a6 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java @@ -0,0 +1,134 @@ +package de.muehlencord.shared.account.business.account.entity; + +import java.io.Serializable; +import java.util.Date; +import java.util.UUID; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import javax.xml.bind.annotation.XmlRootElement; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; + +/** + * + * @author Joern Muehlencord + */ +@Entity +@Table(name = "api_key") +@XmlRootElement +@NamedQueries({ + @NamedQuery(name = "ApiKeyEntity.findAll", query = "SELECT a FROM ApiKeyEntity a"), + @NamedQuery(name = "ApiKeyEntity.findByApiKey", query = "SELECT a FROM ApiKeyEntity a WHERE a.apiKey = :apiKey"), + @NamedQuery(name = "ApiKeyEntity.findByIssuedOn", query = "SELECT a FROM ApiKeyEntity a WHERE a.issuedOn = :issuedOn"), + @NamedQuery(name = "ApiKeyEntity.findByAccount", query = "SELECT a FROM ApiKeyEntity a WHERE a.account = :account"), + @NamedQuery(name = "ApiKeyEntity.findByExpiration", query = "SELECT a FROM ApiKeyEntity a WHERE a.expiration = :expiration")}) +public class ApiKeyEntity implements Serializable { + + private static final long serialVersionUID = -1044658457228215810L; + + @Id + @Basic(optional = false) + @NotNull + @Column(name = "id") + @GeneratedValue(generator = "uuid2") + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @Type(type = "pg-uuid") + private UUID id; + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 200) + @Column(name = "api_key") + private String apiKey; + @Basic(optional = false) + @NotNull + @Column(name = "issued_on") + @Temporal(TemporalType.TIMESTAMP) + private Date issuedOn; + @Column(name = "expiration") + private Short expiration; + @JoinColumn(name = "account", referencedColumnName = "id") + @ManyToOne(optional = false) + private AccountEntity account; + + public ApiKeyEntity() { + // empty constructor required for JPA + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public Date getIssuedOn() { + return issuedOn; + } + + public void setIssuedOn(Date issuedOn) { + this.issuedOn = issuedOn; + } + + public Short getExpiration() { + return expiration; + } + + public void setExpiration(Short expiration) { + this.expiration = expiration; + } + + public AccountEntity getAccount() { + return account; + } + + public void setAccount(AccountEntity account) { + this.account = account; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof ApiKeyEntity)) { + return false; + } + ApiKeyEntity other = (ApiKeyEntity) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "com.wincornixdorf.pcd.business.account.ApiKeyEntity[ id=" + id + " ]"; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyObject.java b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyObject.java new file mode 100644 index 0000000..4e1da31 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyObject.java @@ -0,0 +1,56 @@ +package de.muehlencord.shared.account.business.account.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.google.gson.annotations.Expose; +import java.util.Date; + +/** + * + * @author Joern Muehlencord + */ +public class ApiKeyObject { + + @Expose + private String userName; + @Expose + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm'Z'") + private Date issuedOn; + @Expose + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm'Z'") + private Date expiresOn; + @Expose + private String authToken; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Date getIssuedOn() { + return issuedOn; + } + + public void setIssuedOn(Date issuedOn) { + this.issuedOn = issuedOn; + } + + public Date getExpiresOn() { + return expiresOn; + } + + public void setExpiresOn(Date expiresOn) { + this.expiresOn = expiresOn; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/entity/JWTObject.java b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/JWTObject.java new file mode 100644 index 0000000..ee14b86 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/JWTObject.java @@ -0,0 +1,47 @@ +/* + * 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.business.account.entity; + +/** + * + * @author Joern Muehlencord + */ +public class JWTObject { + + private String userName; + private String unqiueId; + private boolean valid; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUnqiueId() { + return unqiueId; + } + + public void setUnqiueId(String unqiueId) { + this.unqiueId = unqiueId; + } + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + + + + + +}