feat: rewrite LicenseService to mint Ed25519-signed tokens

Replaces UUID token generation with LicenseMinter.mint() from
cameleer-license-minter. Adds full-control generateLicense() overload
accepting custom limits, expiry, grace period, and label.
Adds verifyToken() and verifyTokenSignature() using LicenseValidator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 17:22:05 +02:00
parent 2fd14165bc
commit fdc7187424

View File

@@ -1,12 +1,18 @@
package net.siegeln.cameleer.saas.license;
import com.cameleer.license.minter.LicenseMinter;
import com.cameleer.server.core.license.LicenseInfo;
import com.cameleer.server.core.license.LicenseValidator;
import net.siegeln.cameleer.saas.audit.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.tenant.TenantEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -14,28 +20,52 @@ import java.util.UUID;
@Service
public class LicenseService {
private static final Logger log = LoggerFactory.getLogger(LicenseService.class);
private final LicenseRepository licenseRepository;
private final AuditService auditService;
private final SigningKeyService signingKeyService;
public LicenseService(LicenseRepository licenseRepository, AuditService auditService) {
public LicenseService(LicenseRepository licenseRepository,
AuditService auditService,
SigningKeyService signingKeyService) {
this.licenseRepository = licenseRepository;
this.auditService = auditService;
this.signingKeyService = signingKeyService;
}
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
/**
* Mint an Ed25519-signed license with full control over limits.
*/
public LicenseEntity generateLicense(TenantEntity tenant,
Map<String, Integer> limits,
Instant expiresAt,
int gracePeriodDays,
String label,
UUID actorId) {
Instant now = Instant.now();
Instant expiresAt = now.plus(validity);
UUID licenseId = UUID.randomUUID();
String token = UUID.randomUUID().toString();
LicenseInfo info = new LicenseInfo(
licenseId,
tenant.getSlug(),
label,
limits,
now,
expiresAt,
gracePeriodDays
);
String token = LicenseMinter.mint(info, signingKeyService.getPrivateKey());
var entity = new LicenseEntity();
entity.setTenantId(tenant.getId());
entity.setTier(tenant.getTier().name());
entity.setLimits(new java.util.HashMap<>(limits));
entity.setLabel(label);
entity.setLimits(new HashMap<>(limits));
entity.setGracePeriodDays(gracePeriodDays);
entity.setIssuedAt(now);
entity.setExpiresAt(expiresAt);
entity.setGracePeriodDays(LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS);
entity.setToken(token);
var saved = licenseRepository.save(entity);
@@ -47,6 +77,17 @@ public class LicenseService {
return saved;
}
/**
* Convenience overload using tier presets and default validity.
*/
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
Instant expiresAt = Instant.now().plus(validity);
String label = tenant.getName() + " (" + tenant.getTier().name() + ")";
return generateLicense(tenant, limits, expiresAt,
LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS, label, actorId);
}
public Optional<LicenseEntity> getActiveLicense(UUID tenantId) {
return licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId);
}
@@ -63,18 +104,41 @@ public class LicenseService {
}
/**
* Verifies a license token by checking its existence and validity in the database.
* Returns the license entity's metadata as a map if found and not expired/revoked,
* or empty if the token is unknown or invalid.
* Verify a signed license token using the stored public key.
* Returns the parsed LicenseInfo if valid, or empty if invalid.
*/
public Optional<Map<String, Object>> verifyLicenseToken(String token) {
return licenseRepository.findByToken(token)
.filter(e -> e.getRevokedAt() == null)
.filter(e -> e.getExpiresAt() == null || Instant.now().isBefore(e.getExpiresAt()))
.map(e -> Map.<String, Object>of(
"tenant_id", e.getTenantId().toString(),
"tier", e.getTier(),
"limits", e.getLimits()
));
public Optional<LicenseInfo> verifyToken(String token, String expectedTenantId) {
try {
String publicKeyB64 = signingKeyService.getPublicKeyBase64();
LicenseValidator validator = new LicenseValidator(publicKeyB64, expectedTenantId);
LicenseInfo info = validator.validate(token);
return Optional.of(info);
} catch (Exception e) {
log.debug("License token verification failed: {}", e.getMessage());
return Optional.empty();
}
}
/**
* Verify a signed license token without tenant ID check (for vendor verify tool).
* Decodes the payload and validates the signature only.
*/
public Optional<LicenseInfo> verifyTokenSignature(String token) {
try {
// Decode the payload portion to extract tenantId, then validate
String payloadB64 = token.split("\\.", 2)[0];
String payloadJson = new String(java.util.Base64.getDecoder().decode(payloadB64));
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
var tree = mapper.readTree(payloadJson);
String tenantId = tree.has("tid") ? tree.get("tid").asText() : tree.get("tenantId").asText();
String publicKeyB64 = signingKeyService.getPublicKeyBase64();
LicenseValidator validator = new LicenseValidator(publicKeyB64, tenantId);
LicenseInfo info = validator.validate(token);
return Optional.of(info);
} catch (Exception e) {
log.debug("License token signature verification failed: {}", e.getMessage());
return Optional.empty();
}
}
}