migrated API key support from PCD

This commit is contained in:
2018-11-16 16:21:43 +01:00
parent b2c2619dc4
commit 46f2827338
7 changed files with 505 additions and 11 deletions

View File

@ -72,6 +72,10 @@
<version>1.1-SNAPSHOT</version> <version>1.1-SNAPSHOT</version>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency> <dependency>
<groupId>javax</groupId> <groupId>javax</groupId>
<artifactId>javaee-api</artifactId> <artifactId>javaee-api</artifactId>

View File

@ -3,7 +3,7 @@
CAUTION: Do not modify this file unless you know what you are doing. CAUTION: Do not modify this file unless you know what you are doing.
Unexpected results may occur if the code is changed deliberately. Unexpected results may occur if the code is changed deliberately.
--> -->
<dbmodel pgmodeler-ver="0.9.1" author="Joern Muehlencord" last-position="315,0" last-zoom="0.7" <dbmodel pgmodeler-ver="0.9.1" author="Joern Muehlencord" last-position="46,0" last-zoom="0.6"
default-schema="public"> default-schema="public">
<database name="account_test" encoding="UTF8" lc-collate="C" lc-ctype="C" is-template="false" allow-conns="true" sql-disabled="true"> <database name="account_test" encoding="UTF8" lc-collate="C" lc-ctype="C" is-template="false" allow-conns="true" sql-disabled="true">
</database> </database>
@ -13,7 +13,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="config" hide-ext-attribs="true"> <table name="config" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="1880" y="380"/> <position x="1655" y="520"/>
<column name="application" not-null="true"> <column name="application" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -36,7 +36,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="application_role" hide-ext-attribs="true"> <table name="application_role" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="160" y="500"/> <position x="5" y="450"/>
<column name="id" not-null="true"> <column name="id" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -59,7 +59,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="account" hide-ext-attribs="true"> <table name="account" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="1240" y="100"/> <position x="1020" y="795"/>
<column name="id" not-null="true"> <column name="id" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -100,7 +100,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="account_history" hide-ext-attribs="true"> <table name="account_history" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="760" y="340"/> <position x="230" y="930"/>
<column name="id" not-null="true"> <column name="id" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -129,7 +129,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="account_role" hide-ext-attribs="true"> <table name="account_role" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="740" y="100"/> <position x="540" y="620"/>
<column name="account" not-null="true"> <column name="account" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -143,7 +143,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="application_permission" hide-ext-attribs="true"> <table name="application_permission" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="1560" y="740"/> <position x="1290" y="180"/>
<column name="id" not-null="true"> <column name="id" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -166,7 +166,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="role_permission" hide-ext-attribs="true"> <table name="role_permission" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="960" y="800"/> <position x="585" y="165"/>
<column name="application_role" not-null="true"> <column name="application_role" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -180,7 +180,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="mail_template" hide-ext-attribs="true"> <table name="mail_template" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="280" y="860"/> <position x="290" y="1235"/>
<column name="template_name" not-null="true"> <column name="template_name" not-null="true">
<type name="character varying" length="40"/> <type name="character varying" length="40"/>
</column> </column>
@ -194,7 +194,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="application" hide-ext-attribs="true"> <table name="application" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="1080" y="560"/> <position x="840" y="415"/>
<column name="id" not-null="true"> <column name="id" not-null="true">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -212,7 +212,7 @@ CAUTION: Do not modify this file unless you know what you are doing.
<table name="account_login" hide-ext-attribs="true"> <table name="account_login" hide-ext-attribs="true">
<schema name="public"/> <schema name="public"/>
<position x="1880" y="20"/> <position x="1670" y="755"/>
<column name="id" not-null="true" default-value="uuid_generate_v4()"> <column name="id" not-null="true" default-value="uuid_generate_v4()">
<type name="uuid" length="0"/> <type name="uuid" length="0"/>
</column> </column>
@ -260,6 +260,29 @@ CAUTION: Do not modify this file unless you know what you are doing.
</constraint> </constraint>
</table> </table>
<table name="api_key">
<schema name="public"/>
<position x="1612.5" y="1057.5"/>
<column name="id" not-null="true">
<type name="uuid" length="0"/>
</column>
<column name="account" not-null="true">
<type name="uuid" length="0"/>
</column>
<column name="api_key" not-null="true">
<type name="varchar" length="200"/>
</column>
<column name="issued_on" not-null="true" default-value="timezone('utc'::text, now())">
<type name="timestamptz" length="0"/>
</column>
<column name="expiration">
<type name="smallint" length="0"/>
</column>
<constraint name="api_key_pk" type="pk-constr" table="public.api_key">
<columns names="id" ref-type="src-columns"/>
</constraint>
</table>
<constraint name="config_key_account_fk" type="fk-constr" comparison-type="MATCH FULL" <constraint name="config_key_account_fk" type="fk-constr" comparison-type="MATCH FULL"
upd-action="NO ACTION" del-action="NO ACTION" ref-table="public.account" table="public.config"> upd-action="NO ACTION" del-action="NO ACTION" ref-table="public.account" table="public.config">
<columns names="config_key_account" ref-type="src-columns"/> <columns names="config_key_account" ref-type="src-columns"/>
@ -320,6 +343,12 @@ CAUTION: Do not modify this file unless you know what you are doing.
<columns names="id" ref-type="dst-columns"/> <columns names="id" ref-type="dst-columns"/>
</constraint> </constraint>
<constraint name="api_key_account_fk" type="fk-constr" comparison-type="MATCH FULL"
upd-action="NO ACTION" del-action="NO ACTION" ref-table="public.account" table="public.api_key">
<columns names="account" ref-type="src-columns"/>
<columns names="id" ref-type="dst-columns"/>
</constraint>
<relationship name="rel_account_history_account" type="relfk" <relationship name="rel_account_history_account" type="relfk"
custom-color="#83af1f" custom-color="#83af1f"
src-table="public.account_history" src-table="public.account_history"
@ -375,4 +404,9 @@ CAUTION: Do not modify this file unless you know what you are doing.
dst-table="public.application" reference-fk="config_application_fk" dst-table="public.application" reference-fk="config_application_fk"
src-required="false" dst-required="false"/> src-required="false" dst-required="false"/>
<relationship name="rel_api_key_account" type="relfk"
src-table="public.api_key"
dst-table="public.account" reference-fk="api_key_account_fk"
src-required="false" dst-required="false"/>
</dbmodel> </dbmodel>

View File

@ -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 <joern at muehlencord.de>
*/
public class ApiKeyException extends Exception {
private static final long serialVersionUID = -8017749942111814929L;
/**
* Creates a new instance of <code>ApiKeyException</code> without detail message.
*/
public ApiKeyException() {
}
/**
* Constructs an instance of <code>ApiKeyException</code> with the specified detail message.
* @param msg the detail message.
*/
public ApiKeyException(String msg) {
super(msg);
}
/**
* Constructs an instance of <code>ApiKeyException</code> 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);
}
}

View File

@ -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 <joern at muehlencord.de>
*/
@Stateless
public class ApiKeyService implements Serializable {
private static final long serialVersionUID = -6981864888118320228L;
private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyService.class);
@Inject
@AccountPU
EntityManager em;
@Inject
AccountControl accountControl;
@Inject
ConfigService configService;
@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<ApiKeyEntity> getUsersApiKeys(AccountEntity account) {
Query query = em.createNamedQuery("ApiKeyEntity.findByAccount");
query.setParameter("account", account);
List<ApiKeyEntity> keys = query.getResultList();
if (keys == null) {
return new ArrayList<>();
} else {
return keys;
}
}
public List<ApiKeyEntity> getUsersApiKeys() {
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<ApiKeyEntity> apiKeys = getUsersApiKeys(userAccount);
Iterator<ApiKeyEntity> 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));
}
}

View File

@ -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 <joern at muehlencord.de>
*/
@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 + " ]";
}
}

View File

@ -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 <joern at muehlencord.de>
*/
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;
}
}

View File

@ -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 <joern at muehlencord.de>
*/
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;
}
}