Skip to content

feat: add token management with bcrypt hashing and aes channel encryption - #191

Closed
BeiBaiNian wants to merge 1 commit into
apache:masterfrom
BeiBaiNian:feat-token-1.3
Closed

feat: add token management with bcrypt hashing and aes channel encryption#191
BeiBaiNian wants to merge 1 commit into
apache:masterfrom
BeiBaiNian:feat-token-1.3

Conversation

@BeiBaiNian

Copy link
Copy Markdown
Contributor

This PR is part of issue #179 "make a UI-first LLM API security gateway". Token management (milestone 1.3) is the core of the "security guardrails" selling point: it issues client-facing virtual keys (sk-xxx), enforces authentication, per-model permissions and rate limiting on the 1.2 unified endpoint /v1/chat/completions, and repays the plaintext-storage tech debt of Channel.ApiKey left over from 1.1 (AES encryption + one-time migration).

What changed

  • Added controllers/token.go: 5 CRUD APIs — GET /api/get-tokens (paginated), GET /api/get-token, POST /api/add-token, POST /api/update-token, POST /api/delete-token. On creation, the secret key is bcrypt-hashed; the plaintext key is returned exactly once in the creation response. List/detail responses only expose the SecretKeyPrefix mask — the key itself never leaves the DB.
  • Added object/token.go: Token model (owner+name composite primary key, following the object/site.go multi-tenant pattern) with SecretKeyHash (bcrypt), SecretKeyPrefix, ExpireTime (RFC3339, empty = never expires), AllowedModels ([]string, JSON-serialized, empty = allow all), RateLimit (int, 0 = unlimited), Status. GetTokenBySecretKeyHash() compares bcrypt hashes across enabled tokens.
  • Added object/aes.go: EncryptApiKey/DecryptApiKey (AES-256-GCM, key from env/config). Channel.ApiKey and Token.SecretKey serve different purposes: the former must be forwarded in plaintext → reversible encryption; the latter is only compared → hash-only storage.
  • Modified object/channel.go: AddChannel/UpdateChannel AES-encrypt the API key before writing; TestChannelConnectivity and GetChannelByModel decrypt on read; added MigrateChannelApiKeys(), an idempotent migration (attempt decryption to detect plaintext, encrypt in place if needed) invoked from InitConfig after table creation, so keys written by 1.1 are upgraded without breaking the forwarding path the moment 1.3 merges.
  • Added object/rate_limit.go: CheckRateLimit in-memory limiter (sync.Map, fixed one-minute windows, lazy cleanup of stale entries; limit <= 0 disables limiting).
  • Modified controllers/proxy.go: ChatCompletions now runs authenticateToken first — extract Authorization: Bearer → GetTokenBySecretKeyHash bcrypt compare → check Status/ExpireTime/AllowedModels/RateLimit, returning 401 (missing/invalid token), 403 (disabled/expired/model not allowed) or 429 (rate limited) as appropriate.
  • Modified routers/router.go: registered the 5 /api/token routes.
  • Modified object/ormer.go: createTable() registers Sync2(new(Token)); InitConfig calls MigrateChannelApiKeys().
  • Frontend (same PR): added TokenListPage.js (list + creation modal: custom name, AllowedModels selectable from the union of existing channels' models, one-time plaintext key display with a one-click copy button), TokenEditPage.js (edit name/model permissions/rate limit/expire time/status; expire time stored as RFC3339 to match the backend time.Parse); backend/TokenBackend.js; menu routes in App.js; en/zh locale entries under the token namespace.

Verification

  • go vet ./... passed (fixed the call-site error caused by the TestChannelConnectivity signature change).
  • yarn run build passed.
  • Local UI testing: creating a token (custom name + model selection) works, the one-time key modal displays and the copy button works; the edit page's Expire Time now saves and re-displays correctly (fixed the DatePicker value always null and the non-RFC3339 dateString defects); list page styling aligns with ChannelListPage (pagination/loading/status tags/tooltips).
  • End-to-end auth chain (curl 401/403/429 states, AES migration DB verification) is deferred to the 1.6 integration milestone.

Note

  • This PR branches off the 1.2 branch (feat-proxy-1.2) and depends on it being merged; ship back-to-back with 1.2.
  • After 1.3 merges, /v1/chat/completions switches from unauthenticated to mandatory Bearer token auth — an intentional behavior change.

@BeiBaiNian
BeiBaiNian force-pushed the feat-token-1.3 branch 3 times, most recently from 0746fb8 to e1c06d0 Compare August 13, 2026 09:43
@BeiBaiNian BeiBaiNian closed this Aug 13, 2026
@BeiBaiNian
BeiBaiNian deleted the feat-token-1.3 branch August 13, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant