6. A middleware
Anyone who can reach the API can delete your tasks. In this chapter you protect every change with an API key: reads stay open, and writes need the key. The check runs before every handler, as a middleware.
Generate it
prea g m api_keymiddleware APIKey created at middleware/api_key.goA middleware runs around a handler. It can let the request continue, with c.Next(), or stop it, with c.Abort().
Read the key from the settings
The key is a setting: different on your machine, in the tests and in production. Add it to config/application.yaml:
todo:
api_key: ${TODO_API_KEY} # 16+ characters; empty leaves the writes open${TODO_API_KEY} reads the environment variable. Add it to your .env, and to .env.example with an empty value, so others know it exists:
TODO_API_KEY=my-local-key-0123456789The middleware is a component, like a service, so it can receive settings. Replace middleware/api_key.go:
// APIKey protects the writes: POST, PATCH and DELETE need the X-API-Key
// header when todo.api_key is set. Reads stay open.
type APIKey struct {
Key string `value:"todo.api_key"`
}
var _ core.MiddlewareComponent = (*APIKey)(nil)The value:"todo.api_key" tag fills the field from the configuration when the app starts.
Check the key once, at startup
A key that is too short is a mistake you want to find before the app serves anyone. Components can have an Init method, which runs once at startup, after their fields are filled:
// Init runs once, at startup: a short key stops the app before it serves.
func (m *APIKey) Init(ctx context.Context) error {
if m.Key != "" && len(m.Key) < 16 {
return errors.New("todo.api_key must have at least 16 characters")
}
return nil
}If Init returns an error, the app doesn't start.
Check every request
func (m *APIKey) Handle(c *gin.Context) {
if m.Key == "" || c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead {
c.Next()
return
}
if subtle.ConstantTimeCompare([]byte(c.GetHeader("X-API-Key")), []byte(m.Key)) != 1 {
core.Error(c, core.NewError(http.StatusUnauthorized, "missing or wrong X-API-Key"))
c.Abort()
return
}
c.Next()
}subtle.ConstantTimeCompare compares the keys without leaking, through timing, how much of a wrong key was right.
Add the imports the file uses: context, crypto/subtle, errors and net/http.
Turn it on for the whole app
Middlewares in preas.Middlewares, in preas/preas.go, run on every request:
// Middlewares run on every request, after Security.
var Middlewares = []any{
core.RequestLogger,
core.Recovery,
middleware.APIKey{},
}Import "todo/middleware" at the top of the file.
Try it
curl -X POST localhost:8000/list -H 'Content-Type: application/json' -d '{"name": "Work"}'{
"text": "missing or wrong X-API-Key",
"code": 2
}curl -X POST localhost:8000/list -H 'Content-Type: application/json' \
-H 'X-API-Key: my-local-key-0123456789' -d '{"name": "Work"}'
# → 201
curl localhost:8000/list
# → 200: reads need no keyNow set TODO_API_KEY=short in .env and save. The restart fails, and the app doesn't serve with a weak key:
level=ERROR msg="prea: startup failed" error="di: *middleware.APIKey.Init: todo.api_key must have at least 16 characters"Put the long key back before you go on.
What you learned
- A middleware runs before handlers and can stop a request.
- A component can read settings with
value:""and check them inInit. - Middlewares in
preas.Middlewarescover the whole app.