Extended security for web front end and added first rest resources
This commit is contained in:
@@ -54,6 +54,24 @@
|
||||
<version>2.5.2.Final</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT Library -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT Library -->
|
||||
|
||||
<!-- Apache POI -->
|
||||
|
||||
|
||||
@@ -20,10 +20,12 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "customer")
|
||||
@NamedQuery(name = Customer.FIND_ALL, query = "select c from Customer c order by c.name")
|
||||
@NamedQuery(name = Customer.FIND_BY_NUMBER, query = "select c from Customer c where c.customerNumber = :cutomerNumber")
|
||||
public class Customer extends AbstractDateEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String SEQUENCE = "customer_seq";
|
||||
public static final String FIND_ALL = "Customer.findAll";
|
||||
public static final String FIND_BY_NUMBER = "Customer.findByNumber";
|
||||
public static final String PARAM_NUMBER = "cutomerNumber";
|
||||
|
||||
@@ -39,8 +41,7 @@ public class Customer extends AbstractDateEntity {
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@JoinColumn(name = "customer_id_fk")
|
||||
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<Picture> pictures = new HashSet<>();
|
||||
|
||||
public Long getCustomerId() {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "jwt_refresh_token")
|
||||
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH_REVOKE_NULL, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash AND t.revokedAt IS NULL")
|
||||
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash")
|
||||
@NamedQuery(name = JwtRefreshToken.REVOKE_ALL_USER, query = "UPDATE JwtRefreshToken t SET t.revokedAt = :date WHERE t.user.userId = :userId AND t.revokedAt IS NULL")
|
||||
public class JwtRefreshToken extends AbstractEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String SEQUENCE = "jwt_refresh_token_seq";
|
||||
public static final String FIND_BY_HASH_REVOKE_NULL = "JwtRefreshToken.forHashRevokeNull";
|
||||
public static final String FIND_BY_HASH = "JwtRefreshToken.forHash";
|
||||
public static final String REVOKE_ALL_USER = "JwtRefreshToken.revokeAllUser";
|
||||
public static final String PARAM_HASH = "hash";
|
||||
public static final String PARAM_USER_ID = "userId";
|
||||
public static final String PARAM_DATE = "date";
|
||||
|
||||
@Id
|
||||
@Column(name = "jwt_refresh_token_id", length = 22)
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
|
||||
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
|
||||
private Long jwsRefreshTokenId;
|
||||
|
||||
@Column(name = "token_hash", nullable = false)
|
||||
private String tokenHash;
|
||||
|
||||
@Column(name = "device_info", nullable = false)
|
||||
private String deviceInfo;
|
||||
|
||||
@Column(name = "ip_address", nullable = false)
|
||||
private String ipAddress;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "issued_at", nullable = false)
|
||||
@JsonIgnore
|
||||
private Date issuedAt;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
@JsonIgnore
|
||||
private Date expiresAt;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "revoked_at", nullable = false)
|
||||
@JsonIgnore
|
||||
private Date revokedAt;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "last_used_at", nullable = false)
|
||||
@JsonIgnore
|
||||
private Date lastUsedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "user_id_fk", nullable = true)
|
||||
private User user;
|
||||
|
||||
public Long getJwsRefreshTokenId() {
|
||||
return jwsRefreshTokenId;
|
||||
}
|
||||
|
||||
public void setJwsRefreshTokenId(Long jwsRefreshTokenId) {
|
||||
this.jwsRefreshTokenId = jwsRefreshTokenId;
|
||||
}
|
||||
|
||||
public String getTokenHash() {
|
||||
return tokenHash;
|
||||
}
|
||||
|
||||
public void setTokenHash(String tokenHash) {
|
||||
this.tokenHash = tokenHash;
|
||||
}
|
||||
|
||||
public String getDeviceInfo() {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
public void setDeviceInfo(String deviceInfo) {
|
||||
this.deviceInfo = deviceInfo;
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public void setIpAddress(String ipAddress) {
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
public Date getIssuedAt() {
|
||||
return issuedAt;
|
||||
}
|
||||
|
||||
public void setIssuedAt(Date issuedAt) {
|
||||
this.issuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public Date getRevokedAt() {
|
||||
return revokedAt;
|
||||
}
|
||||
|
||||
public void setRevokedAt(Date revokedAt) {
|
||||
this.revokedAt = revokedAt;
|
||||
}
|
||||
|
||||
public Date getLastUsedAt() {
|
||||
return lastUsedAt;
|
||||
}
|
||||
|
||||
public void setLastUsedAt(Date lastUsedAt) {
|
||||
this.lastUsedAt = lastUsedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private JwtRefreshToken instance = new JwtRefreshToken();
|
||||
|
||||
public Builder tokenHash(String tokenHash) {
|
||||
instance.setTokenHash(tokenHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder deviceInfo(String deviceInfo) {
|
||||
instance.setDeviceInfo(deviceInfo);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder ipAddress(String ipAddress) {
|
||||
instance.setIpAddress(ipAddress);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder expiresAt(Date expiresAt) {
|
||||
instance.setExpiresAt(expiresAt);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder issuedAt(Date issuedAt) {
|
||||
instance.setIssuedAt(issuedAt);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder revokedAt(Date revokedAt) {
|
||||
instance.setRevokedAt(revokedAt);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lastUsedAt(Date lastUsedAt) {
|
||||
instance.setLastUsedAt(lastUsedAt);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder user(User user) {
|
||||
instance.setUser(user);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JwtRefreshToken build() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,17 @@ public class Picture extends AbstractDateEntity {
|
||||
@Column(name = "picture_date", nullable = false)
|
||||
private Date pictureDate;
|
||||
|
||||
@Basic(fetch = FetchType.LAZY)
|
||||
private String comment;
|
||||
|
||||
@Column(name = "image")
|
||||
@Basic(fetch = FetchType.LAZY)
|
||||
private String image;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "customer_id_fk")
|
||||
private Customer customer;
|
||||
|
||||
public Long getPictureId() {
|
||||
return pictureId;
|
||||
}
|
||||
@@ -82,6 +88,14 @@ public class Picture extends AbstractDateEntity {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public Customer getCustomer() {
|
||||
return customer;
|
||||
}
|
||||
|
||||
public void setCustomer(Customer customer) {
|
||||
this.customer = customer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder().append(pictureId).toHashCode();
|
||||
@@ -117,6 +131,11 @@ public class Picture extends AbstractDateEntity {
|
||||
instance.setImage(image);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder customer(Customer customer) {
|
||||
instance.setCustomer(customer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Picture build() {
|
||||
return instance;
|
||||
|
||||
@@ -20,9 +20,12 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "x_user")
|
||||
@NamedQuery(name = User.BY_USERNAME, query = "select u from User u where u.username like :username")
|
||||
public class User extends AbstractDateEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String SEQUENCE = "user_seq";
|
||||
public static final String BY_USERNAME = "user.byUsername";
|
||||
public static final String PARAM_USERNAME = "username";
|
||||
|
||||
@Id
|
||||
@Column(name = "user_id", length = 22)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.ejb.EJBContext;
|
||||
import jakarta.ejb.SessionContext;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
|
||||
public abstract class AbstractService {
|
||||
|
||||
@Resource
|
||||
protected EJBContext ejbContext;
|
||||
|
||||
@PersistenceContext
|
||||
protected EntityManager entityManager;
|
||||
|
||||
@Resource
|
||||
protected SessionContext sessionContext;
|
||||
|
||||
@EJB
|
||||
protected QueryService queryService;
|
||||
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.ejb.LocalBean;
|
||||
import jakarta.ejb.Stateless;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
|
||||
|
||||
/**
|
||||
@@ -24,25 +23,26 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
|
||||
*/
|
||||
@Stateless
|
||||
@LocalBean
|
||||
public class CustomerPictureService {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
@EJB
|
||||
private QueryService queryService;
|
||||
@PermitAll
|
||||
public class CustomerPictureService extends AbstractService {
|
||||
|
||||
public boolean addCustomerPicture(CustomerPictureValue customerPictureValue) {
|
||||
Optional<Customer> customerOpt = queryService.callNamedQuerySingleResult(Customer.FIND_BY_NUMBER, new Param(Customer.PARAM_NUMBER, customerPictureValue.customerNumber()));
|
||||
Customer customer = customerOpt.orElseGet(() -> new Customer.Builder().customerNumber(customerPictureValue.customerNumber()).name(customerPictureValue.pharmacyName()).build());
|
||||
|
||||
Picture picture = new Picture.Builder().username(customerPictureValue.username()).comment(customerPictureValue.comment()).image(customerPictureValue.base64String()).pictureDate(customerPictureValue.date()).build();
|
||||
customer = entityManager.merge(customer);
|
||||
|
||||
Picture picture = new Picture.Builder().customer(customer).username(customerPictureValue.username()).comment(customerPictureValue.comment()).image(customerPictureValue.base64String()).pictureDate(customerPictureValue.date()).build();
|
||||
customer.getPictures().add(picture);
|
||||
|
||||
entityManager.persist(picture);
|
||||
entityManager.merge(customer);
|
||||
entityManager.flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<CustomerListValue> getAll(String query) {
|
||||
// FIXME: do query
|
||||
List<Customer> customers = queryService.callNamedQueryList(Customer.FIND_ALL);
|
||||
customers.forEach(c -> c.getPictures().size());
|
||||
return customers.parallelStream().map(c -> CustomerListValue.builder(c)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.service;
|
||||
|
||||
import static marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.inject.Inject;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.Right;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.utils.JwtTokenUtil;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
|
||||
public class JwtTokenService extends AbstractService {
|
||||
private static final Long EXPIRED_DELTA = 30L * 24 * 60 * 60 * 1000;
|
||||
|
||||
@Inject
|
||||
private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
public TokenPairValue generateTokenPair(User user, String deviceInfo, String ipAddress) {
|
||||
String accessToken = jwtTokenUtil.generateAccessToken(
|
||||
user.getUserId(),
|
||||
user.getUsername(),
|
||||
user.getRights().stream().map(Right::getCode).toList());
|
||||
|
||||
String refreshToken = jwtTokenUtil.generateRefreshToken(user.getUserId());
|
||||
|
||||
// Store refresh token (optional - for revocation support)
|
||||
var refreshTokenEntity = new JwtRefreshToken.Builder()
|
||||
.user(user)
|
||||
.tokenHash(hashToken(refreshToken))
|
||||
.deviceInfo(deviceInfo)
|
||||
.ipAddress(ipAddress)
|
||||
.issuedAt(new Date())
|
||||
.expiresAt(new Date(System.currentTimeMillis() + EXPIRED_DELTA)).build();
|
||||
entityManager.persist(refreshTokenEntity);
|
||||
|
||||
return new TokenPairValue(accessToken, refreshToken);
|
||||
}
|
||||
|
||||
public String refreshAccessToken(String refreshToken) throws Exception {
|
||||
Claims claims = jwtTokenUtil.validateAndExtractClaims(refreshToken);
|
||||
Long userId = Long.parseLong(claims.getSubject());
|
||||
|
||||
// Verify refresh token exists and not revoked
|
||||
String tokenHash = hashToken(refreshToken);
|
||||
|
||||
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH_REVOKE_NULL, new Param(PARAM_HASH, tokenHash));
|
||||
if (tokenEntityOpt.isEmpty()) {
|
||||
// FIXME: do error handling
|
||||
}
|
||||
|
||||
var tokenEntity = tokenEntityOpt.get();
|
||||
tokenEntity.setLastUsedAt(new Date());
|
||||
entityManager.merge(tokenEntity);
|
||||
|
||||
User user = entityManager.find(User.class, userId);
|
||||
return jwtTokenUtil.generateAccessToken(user.getUserId(), user.getUsername(), user.getRights().stream().map(Right::getCode).toList());
|
||||
}
|
||||
|
||||
public void revokeRefreshToken(String refreshToken) {
|
||||
String tokenHash = hashToken(refreshToken);
|
||||
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH, new Param(PARAM_HASH, tokenHash));
|
||||
if (tokenEntityOpt.isPresent()) {
|
||||
JwtRefreshToken tokenEntity = tokenEntityOpt.get();
|
||||
tokenEntity.setRevokedAt(new Date());
|
||||
entityManager.merge(tokenEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public void revokeAllUserTokens(Long userId) {
|
||||
queryService.callNamedQueryUpdate(REVOKE_ALL_USER, new Param(PARAM_DATE, new Date()), new Param(PARAM_USER_ID, userId));
|
||||
}
|
||||
|
||||
private String hashToken(String token) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.ejb.LocalBean;
|
||||
import jakarta.ejb.Stateless;
|
||||
import jakarta.inject.Inject;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
@Stateless
|
||||
@LocalBean
|
||||
public class LoginService extends AbstractService{
|
||||
|
||||
@Inject
|
||||
private JwtTokenService jwtTokenService;
|
||||
|
||||
public TokenPairValue authenticateUser(String username, String deviceInfo, String ipAddress) {
|
||||
// Get logged-in user from database
|
||||
Optional<User> userOpt = queryService.callNamedQuerySingleResult(User.BY_USERNAME, new Param(User.PARAM_USERNAME, username));
|
||||
if (userOpt.isEmpty()) {
|
||||
// FIXME: implement me
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
// Verify user is active
|
||||
if (!user.isActive()) {
|
||||
throw new IllegalArgumentException("User account is inactive");
|
||||
}
|
||||
|
||||
TokenPairValue tokens = jwtTokenService.generateTokenPair(user, deviceInfo, ipAddress);
|
||||
// Logout from the temporary login (we're using tokens now, not session)
|
||||
return tokens;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.utils;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.ejb.LocalBean;
|
||||
import jakarta.ejb.Stateless;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
@Stateless
|
||||
@LocalBean
|
||||
public class JwtTokenUtil {
|
||||
private static final Log LOG = LogFactory.getLog(JwtTokenUtil.class);
|
||||
|
||||
private static final long ACCESS_TOKEN_VALIDITY = 60 * 60 * 1000L; // 1 hour
|
||||
private static final long REFRESH_TOKEN_VALIDITY = 30 * 24 * 60 * 60 * 1000L; // 30 days
|
||||
private static final long TEMP_2FA_TOKEN_VALIDITY = 5 * 60 * 1000L; // 5 minutes
|
||||
|
||||
private static final String ISSUER = "skillmatrix-jwt-issuer";
|
||||
private static final String AUDIENCE = "skillmatrix-api";
|
||||
|
||||
private PrivateKey privateKey;
|
||||
private PublicKey publicKey;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Load key from wildfly system property
|
||||
try {
|
||||
String pem = System.getProperty("jwt.secret.key");
|
||||
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
byte[] decoded = Base64.getDecoder().decode(pem);
|
||||
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
|
||||
privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
|
||||
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
|
||||
LOG.error("Failed to load JWT PrivateKey " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public String generateAccessToken(Long userId, String username, List<String> groups) {
|
||||
return Jwts.builder()
|
||||
.issuer(ISSUER)
|
||||
.audience().add(AUDIENCE).and()
|
||||
.subject(userId.toString())
|
||||
.claim("username", username)
|
||||
.claim("type", "access")
|
||||
.claim("groups", groups)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY))
|
||||
.signWith(privateKey, Jwts.SIG.RS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(Long userId) {
|
||||
return Jwts.builder()
|
||||
.issuer(ISSUER)
|
||||
.audience().add(AUDIENCE).and()
|
||||
.subject(userId.toString())
|
||||
.claim("type", "refresh")
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_VALIDITY))
|
||||
.signWith(privateKey, Jwts.SIG.RS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateTemp2FAToken(Long userId) {
|
||||
return Jwts.builder()
|
||||
.issuer(ISSUER)
|
||||
.audience().add(AUDIENCE).and()
|
||||
.subject(userId.toString())
|
||||
.claim("type", "temp_2fa")
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + TEMP_2FA_TOKEN_VALIDITY))
|
||||
.signWith(privateKey, Jwts.SIG.RS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Claims validateAndExtractClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(publicKey) // FIXME: not working need public key that we currently didn't load
|
||||
.build()
|
||||
.parseUnsecuredClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
public Long extractUserId(String token) {
|
||||
Claims claims = validateAndExtractClaims(token);
|
||||
return Long.parseLong(claims.getSubject());
|
||||
}
|
||||
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Claims claims = validateAndExtractClaims(token);
|
||||
return claims.getExpiration().before(new Date());
|
||||
} catch (JwtException e) {
|
||||
LOG.warn("Failed to get expiration date from token " + e.getMessage());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,14 @@ public class LoginUtils {
|
||||
|
||||
private Optional<SecurityIdentity> authenticate(String username, String password) {
|
||||
try {
|
||||
LOG.error("Login with username: " + username + " password: " + password);
|
||||
Principal principal = new NamePrincipal(username);
|
||||
PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray());
|
||||
|
||||
SecurityDomain sd = SecurityDomain.getCurrent();
|
||||
return Optional.ofNullable(sd.authenticate(principal, evidence));
|
||||
SecurityIdentity identity = sd.authenticate(principal, evidence);
|
||||
LOG.error("Login identity: " + identity);
|
||||
return Optional.ofNullable(identity);
|
||||
} catch (RealmUnavailableException | SecurityException e) {
|
||||
LOG.warn("Failed to authenticate user " + e.getMessage(), e);
|
||||
return Optional.empty();
|
||||
|
||||
@@ -58,11 +58,7 @@ public class CustomerPictureResource {
|
||||
return Response.status(Status.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
|
||||
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
|
||||
|
||||
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest;
|
||||
|
||||
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jboss.resteasy.annotations.GZIP;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
@RequestScoped
|
||||
@Path("customer")
|
||||
public class CustomerResource {
|
||||
private static final Log LOG = LogFactory.getLog(CustomerResource.class);
|
||||
|
||||
@EJB
|
||||
private CustomerPictureService customerPictureService;
|
||||
|
||||
@GZIP
|
||||
@GET
|
||||
@Path("")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Get customer list")
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = CustomerListValue.class))))
|
||||
public Response doAddCustomerPicture(@QueryParam("query") String query) {
|
||||
LOG.debug("Query customers for query " + query);
|
||||
var retVal = customerPictureService.getAll(query);
|
||||
return Response.ok().entity(retVal).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest;
|
||||
|
||||
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.wildfly.security.auth.server.SecurityIdentity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.service.LoginService;
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.utils.LoginUtils;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
|
||||
@RequestScoped
|
||||
@Path("login")
|
||||
@PermitAll
|
||||
public class LoginResource {
|
||||
private static final Log LOG = LogFactory.getLog(LoginResource.class);
|
||||
|
||||
@EJB
|
||||
private LoginService loginService;
|
||||
|
||||
@Inject
|
||||
private LoginUtils loginUtils;
|
||||
|
||||
@GET
|
||||
@Path("/")
|
||||
@Produces(JSON_OUT)
|
||||
@Operation(summary = "Get logged in user")
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved logged in user", content = @Content(mediaType = JSON_OUT, schema = @Schema(implementation = TokenPairValue.class)))
|
||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||
public Response doLogin(@Context HttpServletRequest httpServletRequest) {
|
||||
Optional<SecurityIdentity> identity = loginUtils.authenticate(httpServletRequest);
|
||||
if (identity.isEmpty()) {
|
||||
LOG.debug("identity empty login invalid");
|
||||
return Response.status(401).build();
|
||||
}
|
||||
String username = identity.get().getPrincipal().getName();
|
||||
|
||||
LOG.debug("Login valid returning jwt");
|
||||
String deviceInfo = loginUtils.extractDeviceInfo(httpServletRequest);
|
||||
String ipAddress = loginUtils.extractIpAddress(httpServletRequest);
|
||||
|
||||
TokenPairValue tokenPairValue = loginService.authenticateUser(username, deviceInfo, ipAddress);
|
||||
|
||||
//FIXME: check if we can do a logout to free user from WildFly httpServletRequest.logout();
|
||||
return Response.ok(tokenPairValue).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import jakarta.ws.rs.ApplicationPath;
|
||||
import jakarta.ws.rs.core.Application;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.CustomerPictureResource;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.CustomerResource;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.LoginResource;
|
||||
import marketing.heyday.hartmann.fotodocumentation.rest.MonitoringResource;
|
||||
|
||||
/**
|
||||
@@ -42,9 +44,10 @@ public class ApplicationConfigApi extends Application {
|
||||
Set<Class<?>> retVal = new HashSet<>();
|
||||
retVal.add(OpenApiResource.class);
|
||||
retVal.add(ValidatedMessageBodyReader.class);
|
||||
//retVal.add(AuthenticateFilter.class);
|
||||
retVal.add(LoginResource.class);
|
||||
retVal.add(MonitoringResource.class);
|
||||
retVal.add(CustomerPictureResource.class);
|
||||
retVal.add(CustomerResource.class);
|
||||
LOG.info("returning rest api classes " + retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
|
||||
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 19 Jan 2026
|
||||
*/
|
||||
|
||||
public record CustomerListValue(String name, String customerNumber, int amountOfPicture) {
|
||||
|
||||
public static CustomerListValue builder(Customer customer) {
|
||||
if (customer == null) {
|
||||
return null;
|
||||
}
|
||||
return new CustomerListValue(customer.getName(), customer.getCustomerNumber(), customer.getPictures().size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
|
||||
@Schema(name = "TokenPair")
|
||||
public record TokenPairValue(String accessToken, String refreshToken) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TokenPair{" + "accessToken='" + (accessToken != null ? "[REDACTED]" : "null") + '\'' + ", refreshToken='" + (refreshToken != null ? "[REDACTED]" : "null") + '\'' + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<class>marketing.heyday.hartmann.fotodocumentation.core.model.User</class>
|
||||
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Customer</class>
|
||||
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Picture</class>
|
||||
<class>marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken</class>
|
||||
|
||||
<properties>
|
||||
<property name="hibernate.format_sql" value="false" />
|
||||
|
||||
@@ -42,6 +42,28 @@ create table user_to_right (
|
||||
);
|
||||
|
||||
|
||||
-- jwt_refresh_token
|
||||
|
||||
CREATE SEQUENCE jwt_refresh_token_seq START 1 INCREMENT 1;
|
||||
|
||||
CREATE TABLE jwt_refresh_token (
|
||||
jwt_refresh_token_id BIGINT PRIMARY KEY,
|
||||
token_hash VARCHAR(255) NOT NULL, -- SHA-256 hash of refresh token
|
||||
device_info VARCHAR(255), -- Browser/device identifier
|
||||
ip_address VARCHAR(45),
|
||||
issued_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
revoked_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
user_id_fk BIGINT NOT NULL REFERENCES x_user(user_id),
|
||||
UNIQUE (token_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_jwt_refresh_token_user ON jwt_refresh_token(user_id_fk);
|
||||
CREATE INDEX idx_jwt_refresh_token_expires ON jwt_refresh_token(expires_at);
|
||||
|
||||
|
||||
|
||||
-- customer
|
||||
|
||||
create sequence customer_seq start 25;
|
||||
|
||||
@@ -39,11 +39,6 @@ public class SecurityGenerator {
|
||||
|
||||
}
|
||||
|
||||
public byte[] createPassword(String password, String salt) throws NoSuchAlgorithmException {
|
||||
byte[] saltBytes = salt.getBytes(Charset.forName("utf-8"));
|
||||
return createPassword(password, saltBytes);
|
||||
}
|
||||
|
||||
public byte[] createPassword(String password, byte[] salt) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] passwordBytes = password.getBytes(Charset.forName("utf-8"));
|
||||
@@ -66,8 +61,8 @@ public class SecurityGenerator {
|
||||
byte[] salt = createSalt();
|
||||
String saltHash = encode(salt);
|
||||
|
||||
byte[] passwordByte = createPassword(password, salt);
|
||||
String passwordHash = encode(passwordByte);
|
||||
byte[] digest = createPassword(password, salt);
|
||||
String passwordHash = encode(digest);
|
||||
|
||||
System.out.println("Password " + password);
|
||||
System.out.println("PasswordHash " + passwordHash);
|
||||
|
||||
Binary file not shown.
@@ -40,6 +40,34 @@
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<system-properties>
|
||||
<property name="jwt.secret.key" value="-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCK1EvBSGUg/+Id
|
||||
TNnlqXWkWtLypRDW5YtQ1ilT046AQfPyTCK9WGJUqqtxfiOxQ0qKVWsXZd/3JwHP
|
||||
nqpgORxOlkSpCMJo4syflqSwJ/Zqg6nNEQXErNg1L2/6tM7DW3KnNfW5yujvIDm+
|
||||
UJJCbmJtQ6tYaNqygQhL6nvDiP5jMwPmdAgg/dUyHJKrNOvF9znUF0360wNG8x/Q
|
||||
WsmGKPddMD4k7fttA5/GRszRM/WRMbPogsz43PlERchTrYF+nY/4lnD3fNqppJ5u
|
||||
uskj/G86ux4hmb/C9W+uf5NxTtURTv3H5TJW/jPZ424MoGvzQCeQa0lgNDLAIC+W
|
||||
Y81e+ZT9AgMBAAECggEAOu3LbDNZLe0/4zUMZvaMB6Q/15xmbfmIrdsCNuFdoyab
|
||||
sJVNx7adIphBZs7mwqcwHFEOwKNPMp9dnu4YHvkPAXK6mU+tCg1/UxyEMnv8FpFl
|
||||
wbSAkM/XhJfqve4CuBz4qW53rBIr1tkEebrEoqstX3jyYfg8ILoxtdvGBiV/6cYN
|
||||
Oyy01xd0NMz+JwHuk9l9ADHDqMJiPkJ1zsHqoqOsblIHSiFSyqRAJU1ns6/If/pV
|
||||
DI17G2j16lqK9S+fMltJXDfEGhz0A/c9R60nvQObEOtpUCFWS0DP75oZT2AvNqkY
|
||||
/7aLcBu7FPr522+zFzQrIGON1rJXS/qIBQu0x151JwKBgQDCQGAd+t3qn5Pi63xq
|
||||
4GFEMZHqn3wyXhhcwg6yQFKrIr8wP3Mmr8utXC0HdncXb80bavkbV8Ii6ttItWDo
|
||||
eRv3KhJe+83bS0nJc2QQtfKBt1Dg+1TBSsTwQHYKvKSThNcG3ijqtv6Juac+WqfB
|
||||
H/G5lD5yI4xMquWDyuK6hRNkhwKBgQC29dKdDogFg9cdhpArChduVVGaMu0Ifvt+
|
||||
oOsy3IVPeOPlXyeDIINi7tw17+WSwm3gS0TVqamcefYIhaBlmRwrYS8wHiyQiMfX
|
||||
tgthWXtX5z+lw2MdUfAwW6oDRQLVf7YIsas1Loe7KZRoXEbuBeUP/XdpRO1gtUDR
|
||||
gGL6e3OfWwKBgBlGJPthm6QeVTCOMSb6wM0Nog2j6JXpFkRjX2Qj6F2p7LRLXSEo
|
||||
eFi7CITTDhW3jzlFBtpe5byDUDq6lrxInbHgAHnpS1SADD6wy9E8yyvDfTt4mAN6
|
||||
Rft4d6NX/hXPj+at2ycG3kFvLWp4gyEmld3ugt148JU9GxW1vSBFlktbAoGAN1wO
|
||||
TEN3WOPZlS+AM+WrzVC3jkbWffmeM2SRhiQ/mhpkKqUuGXkfCDJqI0/hURTPlkxw
|
||||
GY5qqdQlY9K7A8LeSSnw00huB5W7kkOdEem3bpOkKI4EUXzXhmpV+QNKpjssY1kP
|
||||
Ctp3a2RbaXBybdcOxlXVad7XTKnLYRjN2ii8hX0CgYEApRFzPDU+leHXGIjPZwjA
|
||||
WcZ7IN+B5pdwJUfqulzx73WtOCfuf2J7HQ0pcaOkG2BOxBY1AGtgPDl7071uYvfR
|
||||
hbZlR027QB9GpO8pQKZ98UquAmQNTOBI0k0RX9XZAK2ae60SM8NXFFF1TDZMoKud
|
||||
eZlo8cWlAC5welD3dz1qxEo=
|
||||
-----END PRIVATE KEY-----"/>
|
||||
|
||||
</system-properties>
|
||||
<management>
|
||||
@@ -299,9 +327,27 @@
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
|
||||
<!-- patrick -->
|
||||
<distributed-realm name="fotoDocumentationRealm" realms="fotoDocumentationJwtRealm fotoDocumentationJdbcRealm" />
|
||||
|
||||
<!-- patrick -->
|
||||
<jdbc-realm name="fotoDocumentationRealm" >
|
||||
<token-realm name="fotoDocumentationJwtRealm" principal-claim="username">
|
||||
<jwt issuer="skillmatrix-jwt-issuer" audience="skillmatrix-api"
|
||||
public-key="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAitRLwUhlIP/iHUzZ5al1
|
||||
pFrS8qUQ1uWLUNYpU9OOgEHz8kwivVhiVKqrcX4jsUNKilVrF2Xf9ycBz56qYDkc
|
||||
TpZEqQjCaOLMn5aksCf2aoOpzREFxKzYNS9v+rTOw1typzX1ucro7yA5vlCSQm5i
|
||||
bUOrWGjasoEIS+p7w4j+YzMD5nQIIP3VMhySqzTrxfc51BdN+tMDRvMf0FrJhij3
|
||||
XTA+JO37bQOfxkbM0TP1kTGz6ILM+Nz5REXIU62Bfp2P+JZw93zaqaSebrrJI/xv
|
||||
OrseIZm/wvVvrn+TcU7VEU79x+UyVv4z2eNuDKBr80AnkGtJYDQywCAvlmPNXvmU
|
||||
/QIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
/>
|
||||
</token-realm>
|
||||
|
||||
<!-- patrick -->
|
||||
<jdbc-realm name="fotoDocumentationJdbcRealm" >
|
||||
<principal-query data-source="fotoDocumentationDS" sql="select password, salt, ri.code as Role from x_user u left join user_to_right rtr on rtr.user_id_fk = u.user_id left join x_right ri on rtr.right_id_fk = ri.right_id where username = ?;">
|
||||
<salted-simple-digest-mapper algorithm="password-salt-digest-sha-256" password-index="1" salt-index="2" />
|
||||
<attribute-mapping>
|
||||
@@ -356,7 +402,7 @@
|
||||
<!-- patrick -->
|
||||
<http-authentication-factory name="fotoDocumentation-http-authentication" security-domain="fotoDocumentationDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="BASIC">
|
||||
<mechanism mechanism-name="BEARER_TOKEN">
|
||||
<mechanism-realm realm-name="fotoDocumentationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
|
||||
@@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -29,6 +31,8 @@ public abstract class AbstractRestTest extends AbstractTest {
|
||||
private static final Log LOG = LogFactory.getLog(AbstractRestTest.class);
|
||||
public static final String TEXT_PLAIN = "text/plain";
|
||||
|
||||
private static Map<String, String> bearerToken = new HashMap<>();
|
||||
|
||||
public HttpResponse executeRequest(Request request) throws IOException {
|
||||
var executor = Executor.newInstance();
|
||||
return executor.use(new BasicCookieStore()).execute(request).returnResponse();
|
||||
@@ -39,6 +43,23 @@ public abstract class AbstractRestTest extends AbstractTest {
|
||||
}
|
||||
|
||||
protected String getAuthorization(String user, String pass) {
|
||||
|
||||
if (!bearerToken.containsKey(user)) {
|
||||
String auth = user + ":" + pass;
|
||||
String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
|
||||
String authorization = "Basic " + encoded;
|
||||
|
||||
bearerToken.put(user, getBearerToken(authorization));
|
||||
}
|
||||
|
||||
return "Bearer " + bearerToken.getOrDefault(user, "");
|
||||
}
|
||||
|
||||
protected String getBasicHeader() {
|
||||
return getBasicHeader(username, password);
|
||||
}
|
||||
|
||||
protected String getBasicHeader(String user, String pass) {
|
||||
String auth = user + ":" + pass;
|
||||
String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
|
||||
return "Basic " + encoded;
|
||||
|
||||
@@ -40,22 +40,24 @@ public class CustomerPictureResourceTest extends AbstractRestTest {
|
||||
@Order(1)
|
||||
public void doAddCustomerPicture() throws IOException {
|
||||
LOG.info("doAddCustomerPicture");
|
||||
|
||||
|
||||
String authorization = getBasicHeader();
|
||||
LOG.info("authorization: " + authorization);
|
||||
String path = deploymentURL + PATH;
|
||||
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
|
||||
.addHeader("Authorization", getAuthorization())
|
||||
.addHeader("Authorization", authorization)
|
||||
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
|
||||
|
||||
HttpResponse httpResponse = executeRequest(request);
|
||||
int code = httpResponse.getStatusLine().getStatusCode();
|
||||
assertEquals(200, code);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void doAddCustomerPictureNoAuth() throws IOException {
|
||||
LOG.info("doAddCustomerPicture");
|
||||
|
||||
LOG.info("doAddCustomerPictureNoAuth");
|
||||
|
||||
String path = deploymentURL + PATH;
|
||||
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
|
||||
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
|
||||
@@ -64,4 +66,14 @@ public class CustomerPictureResourceTest extends AbstractRestTest {
|
||||
int code = httpResponse.getStatusLine().getStatusCode();
|
||||
assertEquals(401, code);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
var test = new CustomerPictureResourceTest();
|
||||
test.deploymentURL = "http://localhost:8080/";
|
||||
test.username = "adm";
|
||||
test.password = "x1t0e7Pb49";
|
||||
|
||||
test.doAddCustomerPicture();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 14 Nov 2024
|
||||
*/
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class CustomerResourceTest extends AbstractRestTest {
|
||||
private static final Log LOG = LogFactory.getLog(CustomerResourceTest.class);
|
||||
private static final String PATH = "api/customer";
|
||||
private static final String BASE_UPLOAD = "src/test/resources/upload/";
|
||||
private static final String BASE_DOWNLOAD = "json/CustomerResourceTest-";
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
initDB();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void doGetAll() throws IOException {
|
||||
LOG.info("doGetAll");
|
||||
|
||||
String authorization = getAuthorization();
|
||||
LOG.info("authorization: " + authorization);
|
||||
String path = deploymentURL + PATH;
|
||||
Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
|
||||
.addHeader("Authorization", authorization);
|
||||
|
||||
HttpResponse httpResponse = executeRequest(request);
|
||||
int code = httpResponse.getStatusLine().getStatusCode();
|
||||
assertEquals(200, code);
|
||||
|
||||
|
||||
String text = getResponseText(httpResponse, "doGetAll");
|
||||
String expected = fileToString(BASE_DOWNLOAD + "doGetAll.json");
|
||||
jsonAssert(expected, text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.rest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 21 Jan 2026
|
||||
*/
|
||||
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class LoginResourceTest extends AbstractRestTest {
|
||||
private static final Log LOG = LogFactory.getLog(LoginResourceTest.class);
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void doTestLogin() {
|
||||
LOG.info("doTestLogin");
|
||||
|
||||
String token = getBasicHeader();
|
||||
assertNotNull(token);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
var test = new LoginResourceTest();
|
||||
test.deploymentURL = "http://localhost:8080/";
|
||||
String token = test.getAuthorization("hartmann", "nvlev4YnTi");
|
||||
System.out.println(token);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "Meier Apotheke",
|
||||
"customerNumber": "2345",
|
||||
"amountOfPicture": 1
|
||||
},
|
||||
{
|
||||
"name": "Müller Apotheke",
|
||||
"customerNumber": "1234",
|
||||
"amountOfPicture": 2
|
||||
},
|
||||
{
|
||||
"name": "Schmidt Apotheke",
|
||||
"customerNumber": "3456",
|
||||
"amountOfPicture": 2
|
||||
}
|
||||
]
|
||||
@@ -4,7 +4,7 @@
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Secure</web-resource-name>
|
||||
<url-pattern>/api/login</url-pattern>
|
||||
<url-pattern>/api/customer</url-pattern>
|
||||
|
||||
<http-method>GET</http-method>
|
||||
<http-method>POST</http-method>
|
||||
@@ -17,7 +17,7 @@
|
||||
</security-constraint>
|
||||
|
||||
<login-config>
|
||||
<auth-method>BASIC</auth-method>
|
||||
<auth-method>BEARER_TOKEN</auth-method>
|
||||
<realm-name>fotoDocumentationRealm</realm-name>
|
||||
</login-config>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user