chore: delete dead auth code — users/roles/JWTs/ForwardAuth live in Logto now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-05 12:32:18 +02:00
parent 1397267be5
commit 3929bbb95e
13 changed files with 0 additions and 772 deletions

View File

@@ -1,60 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
if (!jwtService.isTokenValid(token)) {
filterChain.doFilter(request, response);
return;
}
String email = jwtService.extractEmail(token);
var userId = jwtService.extractUserId(token);
var roles = jwtService.extractRoles(token);
var authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
var authentication = new UsernamePasswordAuthenticationToken(
email, userId, authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}

View File

@@ -1,120 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.siegeln.cameleer.saas.config.JwtConfig;
import org.springframework.stereotype.Service;
import java.security.Signature;
import java.time.Instant;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class JwtService {
private final JwtConfig jwtConfig;
private final ObjectMapper objectMapper;
public JwtService(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
this.objectMapper = new ObjectMapper();
}
public String generateToken(UserEntity user) {
try {
String header = base64UrlEncode(objectMapper.writeValueAsBytes(
Map.of("alg", "EdDSA", "typ", "JWT")
));
Instant now = Instant.now();
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("sub", user.getEmail());
payload.put("uid", user.getId().toString());
payload.put("name", user.getName());
payload.put("roles", user.getRoles().stream()
.map(RoleEntity::getName)
.toList());
payload.put("iat", now.getEpochSecond());
payload.put("exp", now.getEpochSecond() + jwtConfig.getExpirationSeconds());
String payloadEncoded = base64UrlEncode(objectMapper.writeValueAsBytes(payload));
String signingInput = header + "." + payloadEncoded;
Signature sig = Signature.getInstance("Ed25519");
sig.initSign(jwtConfig.getPrivateKey());
sig.update(signingInput.getBytes(java.nio.charset.StandardCharsets.UTF_8));
String signature = base64UrlEncode(sig.sign());
return signingInput + "." + signature;
} catch (Exception e) {
throw new RuntimeException("Failed to generate JWT", e);
}
}
public String extractEmail(String token) {
Map<String, Object> payload = parsePayload(token);
return (String) payload.get("sub");
}
public UUID extractUserId(String token) {
Map<String, Object> payload = parsePayload(token);
return UUID.fromString((String) payload.get("uid"));
}
@SuppressWarnings("unchecked")
public Set<String> extractRoles(String token) {
Map<String, Object> payload = parsePayload(token);
List<String> roles = (List<String>) payload.get("roles");
return roles.stream().collect(Collectors.toSet());
}
public boolean isTokenValid(String token) {
try {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
String signingInput = parts[0] + "." + parts[1];
byte[] signatureBytes = base64UrlDecode(parts[2]);
Signature sig = Signature.getInstance("Ed25519");
sig.initVerify(jwtConfig.getPublicKey());
sig.update(signingInput.getBytes(java.nio.charset.StandardCharsets.UTF_8));
if (!sig.verify(signatureBytes)) {
return false;
}
Map<String, Object> payload = parsePayload(token);
long exp = ((Number) payload.get("exp")).longValue();
return Instant.now().getEpochSecond() < exp;
} catch (Exception e) {
return false;
}
}
private Map<String, Object> parsePayload(String token) {
try {
String[] parts = token.split("\\.");
byte[] payloadBytes = base64UrlDecode(parts[1]);
return objectMapper.readValue(payloadBytes, new TypeReference<>() {});
} catch (Exception e) {
throw new RuntimeException("Failed to parse JWT payload", e);
}
}
private String base64UrlEncode(byte[] data) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
private byte[] base64UrlDecode(String data) {
return Base64.getUrlDecoder().decode(data);
}
}

View File

@@ -1,45 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
@Entity
@Table(name = "permissions")
public class PermissionEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "name", nullable = false, unique = true, length = 100)
private String name;
@Column(name = "description")
private String description;
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -1,94 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "roles")
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "name", nullable = false, unique = true, length = 50)
private String name;
@Column(name = "description")
private String description;
@Column(name = "built_in", nullable = false)
private boolean builtIn;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<PermissionEntity> permissions = new HashSet<>();
@PrePersist
protected void onCreate() {
if (createdAt == null) {
createdAt = Instant.now();
}
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isBuiltIn() {
return builtIn;
}
public void setBuiltIn(boolean builtIn) {
this.builtIn = builtIn;
}
public Instant getCreatedAt() {
return createdAt;
}
public Set<PermissionEntity> getPermissions() {
return permissions;
}
public void setPermissions(Set<PermissionEntity> permissions) {
this.permissions = permissions;
}
}

View File

@@ -1,13 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface RoleRepository extends JpaRepository<RoleEntity, UUID> {
Optional<RoleEntity> findByName(String name);
}

View File

@@ -1,122 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "status", nullable = false, length = 20)
private String status = "ACTIVE";
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<RoleEntity> roles = new HashSet<>();
@PrePersist
protected void onCreate() {
Instant now = Instant.now();
if (createdAt == null) {
createdAt = now;
}
if (updatedAt == null) {
updatedAt = now;
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public Set<RoleEntity> getRoles() {
return roles;
}
public void setRoles(Set<RoleEntity> roles) {
this.roles = roles;
}
}

View File

@@ -1,15 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmail(String email);
boolean existsByEmail(String email);
}

View File

@@ -1,43 +0,0 @@
package net.siegeln.cameleer.saas.config;
import net.siegeln.cameleer.saas.auth.JwtService;
import net.siegeln.cameleer.saas.tenant.TenantService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
@RestController
public class ForwardAuthController {
private final JwtService jwtService;
private final TenantService tenantService;
public ForwardAuthController(JwtService jwtService, TenantService tenantService) {
this.jwtService = jwtService;
this.tenantService = tenantService;
}
@GetMapping("/auth/verify")
public ResponseEntity<Void> verify(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).build();
}
String token = authHeader.substring(7);
if (jwtService.isTokenValid(token)) {
String email = jwtService.extractEmail(token);
var userId = jwtService.extractUserId(token);
return ResponseEntity.ok()
.header("X-User-Id", userId.toString())
.header("X-User-Email", email)
.build();
}
return ResponseEntity.status(401).build();
}
}

View File

@@ -1,82 +0,0 @@
package net.siegeln.cameleer.saas.config;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Component
public class JwtConfig {
private static final Logger log = LoggerFactory.getLogger(JwtConfig.class);
@Value("${cameleer.jwt.expiration:86400}")
private long expirationSeconds = 86400;
@Value("${cameleer.jwt.private-key-path:}")
private String privateKeyPath = "";
@Value("${cameleer.jwt.public-key-path:}")
private String publicKeyPath = "";
private KeyPair keyPair;
@PostConstruct
public void init() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
if (privateKeyPath.isEmpty() || publicKeyPath.isEmpty()) {
log.warn("No Ed25519 key files configured — generating ephemeral keys (dev mode)");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
this.keyPair = keyGen.generateKeyPair();
} else {
log.info("Loading Ed25519 keys from {} and {}", privateKeyPath, publicKeyPath);
PrivateKey privateKey = loadPrivateKey(Path.of(privateKeyPath));
PublicKey publicKey = loadPublicKey(Path.of(publicKeyPath));
this.keyPair = new KeyPair(publicKey, privateKey);
}
}
private PrivateKey loadPrivateKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String pem = Files.readString(path)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] decoded = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(decoded));
}
private PublicKey loadPublicKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String pem = Files.readString(path)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] decoded = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(decoded));
}
public PrivateKey getPrivateKey() {
return keyPair.getPrivate();
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public long getExpirationSeconds() {
return expirationSeconds;
}
}

View File

@@ -1,12 +0,0 @@
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_status ON users (status);

View File

@@ -1,25 +0,0 @@
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
built_in BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255)
);
CREATE TABLE role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);

View File

@@ -1,37 +0,0 @@
-- Permissions
INSERT INTO permissions (id, name, description) VALUES
('00000000-0000-0000-0000-000000000001', 'tenant:manage', 'Full tenant administration'),
('00000000-0000-0000-0000-000000000002', 'billing:manage', 'Manage billing and subscriptions'),
('00000000-0000-0000-0000-000000000003', 'team:manage', 'Manage team members and roles'),
('00000000-0000-0000-0000-000000000004', 'apps:manage', 'Deploy, configure, and manage applications'),
('00000000-0000-0000-0000-000000000005', 'apps:deploy', 'Deploy and promote applications'),
('00000000-0000-0000-0000-000000000006', 'secrets:manage', 'Create and rotate secrets'),
('00000000-0000-0000-0000-000000000007', 'observe:read', 'View traces, topology, dashboards'),
('00000000-0000-0000-0000-000000000008', 'observe:debug', 'Use debugger and replay'),
('00000000-0000-0000-0000-000000000009', 'settings:manage', 'Manage tenant settings');
-- Roles
INSERT INTO roles (id, name, description, built_in) VALUES
('10000000-0000-0000-0000-000000000001', 'OWNER', 'Full tenant admin including billing and deletion', true),
('10000000-0000-0000-0000-000000000002', 'ADMIN', 'Manage apps, secrets, team. No billing.', true),
('10000000-0000-0000-0000-000000000003', 'DEVELOPER', 'Deploy apps, view traces, use debugger.', true),
('10000000-0000-0000-0000-000000000004', 'VIEWER', 'Read-only access to dashboards and traces.', true);
-- Owner: all permissions
INSERT INTO role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000001', id FROM permissions;
-- Admin: everything except billing and tenant management
INSERT INTO role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000002', id FROM permissions
WHERE name NOT IN ('tenant:manage', 'billing:manage');
-- Developer: apps, secrets, observe (including debug)
INSERT INTO role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000003', id FROM permissions
WHERE name IN ('apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug');
-- Viewer: observe read-only
INSERT INTO role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000004', id FROM permissions
WHERE name = 'observe:read';

View File

@@ -1,104 +0,0 @@
package net.siegeln.cameleer.saas.auth;
import net.siegeln.cameleer.saas.config.JwtConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JwtServiceTest {
private JwtService jwtService;
@BeforeEach
void setUp() throws Exception {
JwtConfig config = new JwtConfig();
config.init();
jwtService = new JwtService(config);
}
@Test
void generateToken_producesValidJwt() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
assertNotNull(token);
String[] parts = token.split("\\.");
assertEquals(3, parts.length, "JWT should have 3 parts separated by dots");
}
@Test
void extractEmail_returnsCorrectEmail() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
String email = jwtService.extractEmail(token);
assertEquals("test@example.com", email);
}
@Test
void isTokenValid_returnsTrueForValidToken() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
assertTrue(jwtService.isTokenValid(token));
}
@Test
void isTokenValid_returnsFalseForTamperedToken() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
// Tamper with the last 5 characters of the signature
String tampered = token.substring(0, token.length() - 5) + "XXXXX";
assertFalse(jwtService.isTokenValid(tampered));
}
@Test
void extractRoles_returnsUserRoles() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
var roles = jwtService.extractRoles(token);
assertNotNull(roles);
assertTrue(roles.contains("OWNER"));
assertEquals(1, roles.size());
}
@Test
void extractUserId_returnsCorrectId() {
UserEntity user = createUser("test@example.com", "OWNER");
String token = jwtService.generateToken(user);
UUID extractedId = jwtService.extractUserId(token);
assertEquals(user.getId(), extractedId);
}
private UserEntity createUser(String email, String roleName) {
var role = new RoleEntity();
role.setName(roleName);
var user = new UserEntity();
user.setEmail(email);
user.setName("Test User");
user.getRoles().add(role);
try {
var idField = UserEntity.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(user, UUID.randomUUID());
} catch (Exception e) {
throw new RuntimeException(e);
}
return user;
}
}