Skip to content
22 changes: 9 additions & 13 deletions internal/api/license_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ type revokeResponse struct {
RevokedAt time.Time `json:"revoked_at"`
}

// Removed Metadata from activateRequest: users should not provide metadata during activation
type activateRequest struct {
LicenseKey string `json:"license_key"`
Fingerprint string `json:"fingerprint"`
Metadata map[string]any `json:"metadata"`
LicenseKey string `json:"license_key"`
Fingerprint string `json:"fingerprint"`
}

type activateResponse struct {
Expand Down Expand Up @@ -269,15 +269,9 @@ func (s *Server) handleActivate(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
return
}
if req.Metadata == nil {
req.Metadata = map[string]any{}
}
if err := validateMetadata(req.Metadata); err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
return
}

result, err := s.service.ActivateLicense(r.Context(), req.LicenseKey, req.Fingerprint, req.Metadata)
// Call ActivateLicense without user-provided metadata
result, err := s.service.ActivateLicense(r.Context(), req.LicenseKey, req.Fingerprint, nil)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
writeJSON(w, http.StatusNotFound, errorResponse{Error: "license not found"})
Expand All @@ -299,7 +293,8 @@ func (s *Server) handleActivate(w http.ResponseWriter, r *http.Request) {
token := ""
if result.Valid && result.OfflineEnabled {
var err error
token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, result.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds)
// Pass the license's metadata (from result.Metadata) to issueOfflineToken
token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, result.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds, result.Metadata)
if err != nil {
s.writeUnexpectedError(w, "failed to issue offline token", err)
return
Expand Down Expand Up @@ -368,7 +363,8 @@ func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) {
token := ""
if result.Valid && result.OfflineEnabled {
var err error
token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, req.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds)
// Pass the license's metadata (from result.Metadata) to issueOfflineToken
token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, req.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds, result.Metadata)
if err != nil {
s.writeUnexpectedError(w, "failed to refresh offline token", err)
return
Expand Down
34 changes: 24 additions & 10 deletions internal/api/offline_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import (
"simple-license-server/internal/storage"
)

func (s *Server) issueOfflineToken(ctx context.Context, licenseID, slugName, fingerprint string, licenseExpiresAt *time.Time, lifetimeSeconds int) (string, error) {
// issueOfflineToken now accepts metadata
func (s *Server) issueOfflineToken(
ctx context.Context,
licenseID, slugName, fingerprint string,
licenseExpiresAt *time.Time,
lifetimeSeconds int,
metadata map[string]interface{},
) (string, error) {
licenseID = strings.TrimSpace(licenseID)
slugName = strings.TrimSpace(slugName)
fingerprint = strings.TrimSpace(fingerprint)
Expand Down Expand Up @@ -45,13 +52,20 @@ func (s *Server) issueOfflineToken(ctx context.Context, licenseID, slugName, fin
return "", nil
}

return offlinejwt.SignEd25519JWT(activeKey.PrivateKeyEncrypted, s.offlineSigningKey, activeKey.Kid, offlinejwt.TokenParams{
Issuer: s.offlineTokenIssuer,
Audience: s.offlineTokenAudience,
Subject: licenseID,
Slug: slugName,
Fingerprint: fingerprint,
ExpiresAt: expiresAt,
IssuedAt: now,
})
// Pass metadata to TokenParams
return offlinejwt.SignEd25519JWT(
activeKey.PrivateKeyEncrypted,
s.offlineSigningKey,
activeKey.Kid,
offlinejwt.TokenParams{
Issuer: s.offlineTokenIssuer,
Audience: s.offlineTokenAudience,
Subject: licenseID,
Slug: slugName,
Fingerprint: fingerprint,
ExpiresAt: expiresAt,
IssuedAt: now,
Metadata: metadata, // Include metadata
},
)
}
23 changes: 22 additions & 1 deletion internal/domain/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ const (
ReasonFingerprintNotActive Reason = "fingerprint_not_active"
)

// License struct now includes a Metadata field
type License struct {
status Status
expiresAt *time.Time
maxActivations MaxActivations
metadata map[string]interface{}
}

type MaxActivations struct {
Expand All @@ -37,6 +39,7 @@ type RehydrateParams struct {
Status string
ExpiresAt *time.Time
MaxActivations int
Metadata map[string]interface{} // New field for metadata
}

type ActivationResult struct {
Expand All @@ -63,6 +66,18 @@ type DeactivationResult struct {
StatusChanged bool
}

// copyMap creates a deep copy of a map[string]interface{}
func copyMap(m map[string]interface{}) map[string]interface{} {
if m == nil {
return nil
}
copied := make(map[string]interface{}, len(m))
for k, v := range m {
copied[k] = v
}
return copied
}

func Rehydrate(params RehydrateParams) (*License, error) {
status, err := parseStatus(params.Status)
if err != nil {
Expand All @@ -78,9 +93,15 @@ func Rehydrate(params RehydrateParams) (*License, error) {
status: status,
expiresAt: cloneTime(params.ExpiresAt),
maxActivations: maxActivations,
metadata: copyMap(params.Metadata), // Defensive copy of metadata
}, nil
}

// New method to retrieve metadata (returns a defensive copy)
func (l *License) Metadata() map[string]interface{} {
return copyMap(l.metadata)
}

func NewMaxActivations(value int) (MaxActivations, error) {
if value <= 0 {
return MaxActivations{}, fmt.Errorf("max activations must be greater than 0")
Expand Down Expand Up @@ -219,4 +240,4 @@ func cloneTime(t *time.Time) *time.Time {

v := t.UTC()
return &v
}
}
8 changes: 8 additions & 0 deletions internal/offlinejwt/offlinejwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type GeneratedKeyPair struct {
PublicPEM string
}

// TokenParams now includes a Metadata field
type TokenParams struct {
Issuer string
Audience string
Expand All @@ -33,6 +34,7 @@ type TokenParams struct {
Fingerprint string
ExpiresAt time.Time
IssuedAt time.Time
Metadata map[string]interface{} // New field for metadata
}

func GenerateEd25519KeyPair() (GeneratedKeyPair, error) {
Expand Down Expand Up @@ -113,6 +115,7 @@ func DecryptPrivateKey(encryptedPrivateKey, encryptionSecret string) ([]byte, er
return plain, nil
}

// SignEd25519JWT now includes metadata in JWT claims
func SignEd25519JWT(encryptedPrivateKey, encryptionSecret, kid string, params TokenParams) (string, error) {
privatePEM, err := DecryptPrivateKey(encryptedPrivateKey, encryptionSecret)
if err != nil {
Expand Down Expand Up @@ -157,6 +160,11 @@ func SignEd25519JWT(encryptedPrivateKey, encryptionSecret, kid string, params To
claims["aud"] = aud
}

// Include metadata in JWT claims
if params.Metadata != nil {
claims["metadata"] = params.Metadata
}

headerJSON, err := json.Marshal(header)
if err != nil {
return "", fmt.Errorf("marshal jwt header: %w", err)
Expand Down
Loading
Loading