Files
dpsapi/auth.go
T
bryanpedini b792ad256e feat(api): add authenticated containers http api
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.
2026-10-06 10:52:25 +02:00

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
}