API Key Authentication (3 Headers)
Overview
Unlike cms-api and client-api, webhook-go uses no JWT at all. It has exactly one authentication scheme: a three-header api-key designed for B2B integrations.
| Header | Meaning |
|---|---|
x-client-id | Identifies the client, keyed against the api_client table |
x-client-secret | The client's secret, compared as plaintext directly in SQL |
x-api-key | The key that defines access scope, from the api_key table (line_oa_id plus organization_id) |
The design separates "who you are" (client id and secret) from "what you may access" (the api key). A single client can hold multiple api keys, each bound to a different LINE OA.
Only /api/audience and /api/line-oa/verify are protected — four routes in total. Every route
that receives events from LINE or Chatwoot is deliberately left public.
Business Flow
Middleware stage (APIKeyAuth)
- Read all three headers. If any is missing, respond
401 "Missing credentials in headers"and stop immediately. - If the repository is not wired (a boot without a database), respond
500— failing closed rather than letting the request through. - Call
ValidateClient:SELECT ... FROM api_client WHERE client_id=$1 AND client_secret=$2 AND status='active'- A genuine database error must return
500; collapsing it into401is forbidden by the "never swallow an error" rule. - No matching row returns
401 "Invalid client credentials".
- A genuine database error must return
- On success, store
apiClientId(anint64fromapi_client.id) andapiKey(the raw header string) in the gin context. Together they stand in for the original NestJSClsService. - Call
c.Next().
Service stage (scope resolution)
- The handler reads the values back with
middleware.ApiClientID(c)andmiddleware.APIKey(c). Missing values mean the guard was bypassed, which returns401. - The service calls
apikey.Repository.GetByClientAndKey:SELECT ... FROM api_key WHERE api_client_id=$1 AND key=$2 AND status='active' - The resulting
{id, lineOaId, organizationId}scopes every subsequent query.
When no row is found at this stage, each endpoint behaves differently — inconsistent parity
inherited from the original: GET /audience returns 200 null, PUT returns 500, DELETE
returns 400 "Step 1: ...", and verify continues on and eventually returns 404.
Security notes worth recording
client_secretis stored and compared as plaintext in the database, deliberately matching the original. The fileinternal/middleware/apikey.gocarries the comment "plaintext compare — match source; no hashing". Theinternal/middleware/apikey_test.gopackage and the template's bcrypt helper still exist but are not used on the live path.- There is no rate limit specific to authentication failures — only the shared per-IP limit.
- This codebase has no api-key expiry mechanism; only a
statuscolumn.
Key Files & Functions
| File | Highlights |
|---|---|
internal/middleware/apikey.go | APIKeyAuth(v ClientValidator) gin.HandlerFunc, ApiClientID(c), APIKey(c), const CtxKeyAPIClientID / CtxKeyAPIKey, interface ClientValidator |
internal/server/server.go | (*Deps).APIKeyAuth() — binds the middleware to deps.APIClient |
internal/apiclient/repository.go | ValidateClient(ctx, clientID, clientSecret) |
internal/apiclient/entity.go | struct ApiClient |
internal/apikey/repository.go | GetByClientAndKey(ctx, apiClientID, key) |
internal/apikey/entity.go | struct ApiKey (ID, LineOaID, OrganizationID, …), const StatusActive |
cmd/api/main.go | Constructs both repositories when a.SQL != nil |
cmd/api/wiring_test.go | Asserts that four routes are guarded and three remain public |
Protection by route
| Route | Auth |
|---|---|
GET /api, GET /api/health | None |
POST /api/line/:id, POST /api/Line/:id | None |
POST /api/tracking | None |
POST /api/mbox/callback/:oaHash | None |
GET /api/audience | api-key |
PUT /api/audience/:id | api-key |
DELETE /api/audience/:id | api-key |
POST /api/line-oa/verify | api-key |
Connections to Other Services
- Postgres — the
api_clientandapi_keytables, both queried through the pgx simple protocol, withint64identifiers, pointers for nullable columns, and thestatusenum bound as a string. - cms-api — the
api-clientandapi-keymodules provide the screens that issue and revoke these credentials. - Related database domain:
api-client-key. - Consumers of this middleware: Managing Audience Members and LINE OA Verification.
- The full middleware chain is documented in Core HTTP, Middleware Pipeline & Database Access.