151 lines
4.2 KiB
Java
151 lines
4.2 KiB
Java
package com.example.security;
|
|
|
|
import jakarta.persistence.*;
|
|
import org.jspecify.annotations.Nullable;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
@Entity
|
|
@Table(name = "app_user")
|
|
public class User {
|
|
|
|
public static final int USERNAME_MAX_LENGTH = 100;
|
|
|
|
@Id
|
|
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
|
@Column(name = "user_id")
|
|
private Long id;
|
|
|
|
@Column(name = "username", nullable = false, unique = true, length = USERNAME_MAX_LENGTH)
|
|
private String username = "";
|
|
|
|
@Column(name = "password_hash", nullable = false)
|
|
private String passwordHash = "";
|
|
|
|
@Column(name = "enabled", nullable = false)
|
|
private boolean enabled = true;
|
|
|
|
@Column(name = "totp_secret")
|
|
private @Nullable String totpSecret;
|
|
|
|
@Column(name = "two_factor_enabled", nullable = false)
|
|
private boolean twoFactorEnabled = false;
|
|
|
|
// The most recently accepted TOTP time step, tracked to reject replay of an
|
|
// observed code within its own validity window (see TwoFactorService#verify).
|
|
@Column(name = "totp_last_used_step")
|
|
private @Nullable Long totpLastUsedTimeStep;
|
|
|
|
@ElementCollection(fetch = FetchType.EAGER)
|
|
@CollectionTable(name = "app_user_role", joinColumns = @JoinColumn(name = "user_id"))
|
|
@Column(name = "role", nullable = false)
|
|
@Enumerated(EnumType.STRING)
|
|
private Set<Role> roles = new HashSet<>();
|
|
|
|
protected User() { // To keep Hibernate happy
|
|
}
|
|
|
|
public User(String username, String passwordHash) {
|
|
setUsername(username);
|
|
setPasswordHash(passwordHash);
|
|
}
|
|
|
|
public @Nullable Long getId() {
|
|
return id;
|
|
}
|
|
|
|
public String getUsername() {
|
|
return username;
|
|
}
|
|
|
|
public void setUsername(String username) {
|
|
if (username.isBlank()) {
|
|
throw new IllegalArgumentException("Username must not be blank");
|
|
}
|
|
if (username.length() > USERNAME_MAX_LENGTH) {
|
|
throw new IllegalArgumentException("Username length exceeds " + USERNAME_MAX_LENGTH);
|
|
}
|
|
this.username = username;
|
|
}
|
|
|
|
public String getPasswordHash() {
|
|
return passwordHash;
|
|
}
|
|
|
|
public void setPasswordHash(String passwordHash) {
|
|
if (passwordHash.isBlank()) {
|
|
throw new IllegalArgumentException("Password hash must not be blank");
|
|
}
|
|
this.passwordHash = passwordHash;
|
|
}
|
|
|
|
public boolean isEnabled() {
|
|
return enabled;
|
|
}
|
|
|
|
public void setEnabled(boolean enabled) {
|
|
this.enabled = enabled;
|
|
}
|
|
|
|
public Set<Role> getRoles() {
|
|
return roles;
|
|
}
|
|
|
|
public void addRole(Role role) {
|
|
roles.add(role);
|
|
}
|
|
|
|
public @Nullable String getTotpSecret() {
|
|
return totpSecret;
|
|
}
|
|
|
|
public boolean isTwoFactorEnabled() {
|
|
return twoFactorEnabled;
|
|
}
|
|
|
|
public @Nullable Long getTotpLastUsedTimeStep() {
|
|
return totpLastUsedTimeStep;
|
|
}
|
|
|
|
/**
|
|
* Enables two-factor authentication with the given TOTP secret. The caller
|
|
* must have already verified a code against this secret before calling
|
|
* this method — it does not verify anything itself.
|
|
*/
|
|
public void enableTwoFactor(String totpSecret) {
|
|
if (totpSecret.isBlank()) {
|
|
throw new IllegalArgumentException("TOTP secret must not be blank");
|
|
}
|
|
this.totpSecret = totpSecret;
|
|
this.twoFactorEnabled = true;
|
|
this.totpLastUsedTimeStep = null;
|
|
}
|
|
|
|
public void recordAcceptedTotpTimeStep(long timeStep) {
|
|
this.totpLastUsedTimeStep = timeStep;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
|
|
return false;
|
|
}
|
|
if (obj == this) {
|
|
return true;
|
|
}
|
|
|
|
User other = (User) obj;
|
|
return getId() != null && getId().equals(other.getId());
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
// Hashcode should never change during the lifetime of an object. Because of
|
|
// this we can't use getId() to calculate the hashcode. Unless you have sets
|
|
// with lots of entities in them, returning the same hashcode should not be a
|
|
// problem.
|
|
return getClass().hashCode();
|
|
}
|
|
}
|