Skip to content

Token Manager System Design

This system implements a resilient Strategy Pattern wrapped in a Facade. It ensures that the application controllers remain agnostic to where tokens are stored (Browser Cookies vs. SQL Database) and how they are secured (Raw JWT vs. Encrypted Blob).

High-Level Architecture

The system uses a strict "Black Box" approach. External packages cannot access internal strategies directly; they must go through the public Manager interface.

graph TD
    Controller[Auth Controller] -->|Calls Set/Get/Delete| Manager[Manager Interface Facade]

    subgraph "Token Manager Package (Black Box)"
        Manager -->|Route by TokenType| StratMap{Strategy Map}

        StratMap -->|TypeAccess| CookieStrat[Cookie Strategy]
        StratMap -->|TypeRefresh| DBStrat[DB Strategy]

        CookieStrat -->|Set-Cookie| Client[HTTP Response]

        DBStrat -->|Encrypt/Decrypt| Coder[Internal Coder AES]
        DBStrat -->|CRUD| Adapter[Repo Adapter]
    end

    Adapter -->|SQL Queries| DB[(Database)]

Core Design Patterns

1. The Facade (Manager)

The rest of the application interacts only with the Manager interface. This provides a unified API for all token operations.

type Manager interface {
    Set(c *gin.Context, t TokenType, value string, opts ...Option) error
    Get(c *gin.Context, t TokenType, opts ...Option) (string, error)
    DeleteMulti(c *gin.Context, tokens []TokenType, opts ...Option) error
}

2. The Strategy Pattern

Internal logic changes based on the TokenType. - Access Token: Uses cookieStrategy. Stores raw JWTs in stateless, secure, HTTP-only cookies. - Refresh Token: Uses dbStrategy. Encrypts the token and stores it in the database.

3. The Adapter Pattern

To prevent the Token Manager from depending on specific Database Structs (like User), we use Functional Adapters. This bridges the generic (id, string) requirement of the token package to the specific SQL schema of the application.

Token Types & Lifecycle

Token Type Storage Security Lifecycle
TypeAccess Browser Cookie Raw Signed JWT Short-lived (1h). Rotates automatically via middleware.
TypeRefresh SQL Database Encrypted (AES) Long-lived (90d). Used for WorkOS/App sessions.
TypeGoogleRefresh SQL Database Encrypted (AES) Long-lived. Used for background Calendar sync.
TypeAllRefresh Virtual (SQL) N/A Delete Only. Atomic cleanup of all DB sessions.

Optimized Logout Flow

When a user logs out, we need to clear the browser cookie AND remove the WorkOS session from the database, but we might want to keep the Google Calendar token alive for background jobs (or delete that too).

To avoid multiple database calls, we use DeleteMulti with the virtual TypeAllRefresh strategy.

sequenceDiagram
    participant C as Controller
    participant M as TokenManager
    participant CS as CookieStrategy
    participant DS as DBStrategy
    participant DB as Database

    C->>M: DeleteMulti([TypeAccess, TypeAllRefresh], UserID)

    par Cookie Cleanup
        M->>CS: Delete(TypeAccess)
        CS-->>C: Set-Cookie: max-age=-1
    and DB Cleanup
        M->>DS: Delete(TypeAllRefresh)
        DS->>DB: UPDATE users SET workos_token=NULL...
    end

Integration Example

The system is wired in app.go. This is where the specific SQL queries are injected into the generic manager.

// Wire the specific SQL queries to the generic Manager
workOSAdapter := token.NewRepoAdapter(
    // Save
    func(ctx context.Context, uid, val string) error {
        return userRepo.UpdateColumn(ctx, uid, "workos_token", val)
    },
    // Get
    func(ctx context.Context, uid string) (string, error) {
        return userRepo.GetColumn(ctx, uid, "workos_token")
    },
    // Delete
    func(ctx context.Context, uid string) error {
        return userRepo.UpdateColumn(ctx, uid, "workos_token", "")
    }
)

cfg := token.Config{
    MainRefreshRepo: workOSAdapter,
    // ... keys and domain settings
}

manager, _ := token.NewManager(cfg)