Skip to content

v0.5.0 Release Notes

Release Date: 2026-04-05

Highlights

  • Audience-Aware JWT Tokens: Separate BFF (web browser) and API (programmatic) clients with distinct audience claims for strict path separation
  • Ent-Backed Stores: Production-ready database storage for API keys and BFF sessions using Ent ORM
  • Token Encryption: AES-256-GCM encryption for tokens at rest in BFF session storage
  • Reusable Ent Mixins: Drop-in schema mixins for API keys and BFF sessions

Added

Audience-Aware JWT Tokens (session/jwt)

New methods for generating audience-scoped tokens:

  • GenerateBFFTokenPair(): Create token pairs for web clients with aud: "bff"
  • GenerateAPIToken(): Create scoped tokens for API clients with aud: "api"
  • GenerateAccessTokenWithAudience(): Create tokens with custom audiences
  • ValidateAccessTokenWithAudience(): Validate tokens with audience checking

New Claims helpers:

  • Audience(): Get first audience value
  • HasAudience(aud): Check for specific audience
  • WithAudience(...): Set audience (builder pattern)
import "github.com/grokify/coreforge/session/jwt"

// Generate BFF token pair for web clients
pair, err := svc.GenerateBFFTokenPair(userID, email, name)

// Generate API token for programmatic access
token, err := svc.GenerateAPIToken(userID, email, name, []string{"read:users"})

// Validate with audience checking
claims, err := svc.ValidateAccessTokenWithAudience(token, "api")

Token Encryption (session/bff)

AES-256-GCM encryption for tokens at rest:

import "github.com/grokify/coreforge/session/bff"

// Create encryptor with 32-byte key
encryptor, err := bff.NewEncryptor([]byte("your-32-byte-encryption-key-here"))

// Encrypt/decrypt tokens
encrypted, err := encryptor.EncryptString(accessToken)
decrypted, err := encryptor.DecryptString(encrypted)

Ent-Backed BFF Session Store (session/bff)

Production database storage for BFF sessions:

  • Automatic token encryption before storage
  • Configurable expired session cleanup interval
  • EntClientInterface for wrapping your Ent client
import "github.com/grokify/coreforge/session/bff"

store, err := bff.NewEntStore(bff.EntStoreConfig{
    Client:          yourEntClientWrapper,
    Encryptor:       encryptor,
    CleanupInterval: 1 * time.Hour,
})

Ent-Backed API Key Store (identity/apikey)

Production database storage for API keys:

import "github.com/grokify/coreforge/identity/apikey"

store, err := apikey.NewEntStore(apikey.EntStoreConfig{
    Client: yourEntClientWrapper,
})

svc := apikey.NewService(apikey.ServiceConfig{
    Store: store,
})

Ent Schema Mixins (identity/ent/mixin)

Reusable mixins for your Ent schemas:

APIKey Mixin fields:

  • id, name, prefix, key_hash (sensitive)
  • owner_id, organization_id, scopes, description
  • environment (live/test), expires_at, last_used_at, last_used_ip
  • revoked, revoked_at, revoked_reason, metadata, timestamps
  • Indexes: owner_id, organization_id, prefix, key_hash (unique), environment

BFFSession Mixin fields:

  • id, user_id, organization_id
  • access_token_encrypted, refresh_token_encrypted (sensitive)
  • dpop_key_pair_encrypted, dpop_thumbprint
  • ip_address, user_agent, metadata
  • last_accessed_at, expires_at, timestamps
  • Indexes: user_id, expires_at, organization_id
// your-app/ent/schema/api_key.go
package schema

import (
    "entgo.io/ent"
    cfmixin "github.com/grokify/coreforge/identity/ent/mixin"
)

type APIKey struct {
    ent.Schema
}

func (APIKey) Mixin() []ent.Mixin {
    return []ent.Mixin{
        cfmixin.APIKeyMixin{},
    }
}

Documentation

  • JWT audience separation guide (docs/bff/audience.md)
  • Ent-backed session store integration guide (docs/bff/ent-store.md)
  • API key Ent store implementation documentation (docs/identity/api-keys.md)
  • OAuth client package documentation (docs/identity/oauthclient.md)
  • Design documents for JWT audience and Ent stores features

Dependencies

  • github.com/authzed/spicedb: 1.50.0 → 1.51.0
  • entgo.io/ent: 0.14.5 → 0.14.6
  • github.com/danielgtaylor/huma/v2: 2.37.2 → 2.37.3
  • github.com/mattn/go-sqlite3: 1.14.37 → 1.14.38
  • google.golang.org/grpc: 1.79.3 → 1.80.0

Migration Guide

Using Audience-Aware Tokens

  1. Update your BFF to use GenerateBFFTokenPair():
// Before
pair, err := svc.GenerateTokenPair(userID, email, name)

// After - for web clients
pair, err := svc.GenerateBFFTokenPair(userID, email, name)
  1. Update your API middleware to validate audience:
claims, err := svc.ValidateAccessTokenWithAudience(token, "api")
if err != nil {
    // Token invalid or wrong audience
}

Using Ent-Backed Stores

  1. Add the mixin to your Ent schema:
func (APIKey) Mixin() []ent.Mixin {
    return []ent.Mixin{
        cfmixin.APIKeyMixin{},
    }
}
  1. Implement EntClientInterface wrapping your Ent client:
type EntClientWrapper struct {
    client *ent.Client
}

func (w *EntClientWrapper) CreateAPIKey(ctx context.Context, key *apikey.APIKey, keyHash string) error {
    _, err := w.client.APIKey.Create().
        SetID(key.ID).
        SetName(key.Name).
        SetPrefix(key.Prefix).
        SetKeyHash(keyHash).
        // ... set other fields
        Save(ctx)
    return err
}
// Implement other methods...
  1. Create the store:
store, err := apikey.NewEntStore(apikey.EntStoreConfig{
    Client: &EntClientWrapper{client: entClient},
})

Setting Up Token Encryption

  1. Generate a 32-byte encryption key (store securely):
openssl rand -base64 32
  1. Create the encryptor and Ent store:
encryptor, err := bff.NewEncryptor([]byte(os.Getenv("SESSION_ENCRYPTION_KEY")))
if err != nil {
    log.Fatal(err)
}

store, err := bff.NewEntStore(bff.EntStoreConfig{
    Client:    yourEntClientWrapper,
    Encryptor: encryptor,
})

Semver Note

This is a minor version bump (v0.4.0 → v0.5.0) as it adds new features without breaking changes. All existing APIs remain backward compatible.