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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user