GET /api/v1/containers behind bearer token auth, JSON errors for 401/404/405/500, one audit row per authenticated request, graceful shutdown; unit and e2e tests for auth, routing, auditing, request logging and server lifecycle.
82 lines
2.3 KiB
Go
82 lines
2.3 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Authentication result classes; these fixed strings are the only authentication detail ever logged
|
|
const (
|
|
authMissingHeader = "missing_header"
|
|
authEmptyHeader = "empty_header"
|
|
authMultipleHeaders = "multiple_headers"
|
|
authBadScheme = "bad_scheme"
|
|
authEmptyToken = "empty_token"
|
|
authUnknownToken = "unknown_token"
|
|
authInactiveToken = "inactive_token"
|
|
authLookupError = "lookup_error"
|
|
authOK = "ok"
|
|
)
|
|
|
|
// TokenLookup is the token-store dependency of the HTTP layer
|
|
type TokenLookup interface {
|
|
GetToken(ctx context.Context, value string) (Token, error)
|
|
}
|
|
|
|
// parseAuthorization extracts the bearer token from the request headers
|
|
// It returns the token and an empty class on success, or an empty token and the failure class
|
|
// Accepted form: exactly one Authorization header, scheme "Bearer" (ASCII case-insensitive), one space, token
|
|
// The token is taken byte-exact: no trimming or normalization is applied
|
|
func parseAuthorization(h http.Header) (string, string) {
|
|
values := h.Values("Authorization")
|
|
switch {
|
|
case len(values) == 0:
|
|
return "", authMissingHeader
|
|
case len(values) > 1:
|
|
return "", authMultipleHeaders
|
|
case values[0] == "":
|
|
return "", authEmptyHeader
|
|
}
|
|
|
|
// Split "<scheme> <token>" at the first space
|
|
scheme, token, found := strings.Cut(values[0], " ")
|
|
if !asciiEqualFold(scheme, "Bearer") {
|
|
return "", authBadScheme
|
|
}
|
|
if !found || token == "" {
|
|
return "", authEmptyToken
|
|
}
|
|
return token, ""
|
|
}
|
|
|
|
// authenticate validates the request credentials against the token store
|
|
// It returns the authenticated token, the result class, and a non-nil error only when the lookup itself failed
|
|
func authenticate(ctx context.Context, log *slog.Logger, tokens TokenLookup, h http.Header) (Token, string, error) {
|
|
defer traceFunc(ctx, log, "authenticate")()
|
|
|
|
// Parse the header
|
|
value, class := parseAuthorization(h)
|
|
if class != "" {
|
|
return Token{}, class, nil
|
|
}
|
|
|
|
// Look the token up
|
|
tok, err := tokens.GetToken(ctx, value)
|
|
if errors.Is(err, ErrTokenNotFound) {
|
|
return Token{}, authUnknownToken, nil
|
|
}
|
|
if err != nil {
|
|
return Token{}, authLookupError, err
|
|
}
|
|
if !tok.Active {
|
|
return Token{}, authInactiveToken, nil
|
|
}
|
|
return tok, authOK, nil
|
|
}
|