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)