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; 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.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService; import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.tenant.TenantEntity; import net.siegeln.cameleer.saas.tenant.TenantEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -14,28 +20,52 @@ import java.util.UUID;
@Service @Service
public class LicenseService { public class LicenseService {
private static final Logger log = LoggerFactory.getLogger(LicenseService.class);
private final LicenseRepository licenseRepository; private final LicenseRepository licenseRepository;
private final AuditService auditService; 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.licenseRepository = licenseRepository;
this.auditService = auditService; 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 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(); var entity = new LicenseEntity();
entity.setTenantId(tenant.getId()); entity.setTenantId(tenant.getId());
entity.setTier(tenant.getTier().name()); 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.setIssuedAt(now);
entity.setExpiresAt(expiresAt); entity.setExpiresAt(expiresAt);
entity.setGracePeriodDays(LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS);
entity.setToken(token); entity.setToken(token);
var saved = licenseRepository.save(entity); var saved = licenseRepository.save(entity);
@@ -47,6 +77,17 @@ public class LicenseService {
return saved; 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) { public Optional<LicenseEntity> getActiveLicense(UUID tenantId) {
return licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(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. * Verify a signed license token using the stored public key.
* Returns the license entity's metadata as a map if found and not expired/revoked, * Returns the parsed LicenseInfo if valid, or empty if invalid.
* or empty if the token is unknown or invalid.
*/ */
public Optional<Map<String, Object>> verifyLicenseToken(String token) { public Optional<LicenseInfo> verifyToken(String token, String expectedTenantId) {
return licenseRepository.findByToken(token) try {
.filter(e -> e.getRevokedAt() == null) String publicKeyB64 = signingKeyService.getPublicKeyBase64();
.filter(e -> e.getExpiresAt() == null || Instant.now().isBefore(e.getExpiresAt())) LicenseValidator validator = new LicenseValidator(publicKeyB64, expectedTenantId);
.map(e -> Map.<String, Object>of( LicenseInfo info = validator.validate(token);
"tenant_id", e.getTenantId().toString(), return Optional.of(info);
"tier", e.getTier(), } catch (Exception e) {
"limits", e.getLimits() 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();
}
} }
} }