Skip to content

How to Verify Requests (API Keys & JWT/OIDC)

When your tool exposes an HTTP service, you usually need to authenticate callers. pkg/authn provides verifiers (API key, JWT/OIDC, mTLS) and pkg/http wraps them in a fail-closed AuthMiddleware. This guide shows the common setups. For the threat model and verification internals, see the Auth component.

Authenticate with API keys

Build a verifier from one or more KeyEntry secrets, then read them from a header:

import (
    "gitlab.com/phpboyscout/go-tool-base/pkg/authn"
    gtbhttp "gitlab.com/phpboyscout/go-tool-base/pkg/http"
)

verifier, err := authn.NewAPIKeyVerifier(
    authn.KeyEntry{Key: os.Getenv("CI_TOKEN"), Subject: "ci-runner"},
    authn.KeyEntry{Key: os.Getenv("ADMIN_TOKEN"), Subject: "admin"},
)
if err != nil {
    return err
}

auth, err := gtbhttp.AuthMiddleware(
    gtbhttp.WithAPIKeyHeader("X-API-Key", verifier),
)
if err != nil {
    return err
}

// AuthMiddleware is a func(http.Handler) http.Handler — wrap your mux:
http.ListenAndServe(":8080", auth(mux))

Keys are compared in constant time. Requests without a valid key get 401.

Authenticate with JWT / OIDC

Point a JWT verifier at your issuer. With WithOIDCDiscovery the JWKS endpoint is resolved from /.well-known/openid-configuration; otherwise set JWKSURL yourself.

verifier, err := authn.NewJWTVerifier(ctx,
    authn.JWTConfig{
        Issuer:    "https://issuer.example.com",
        Audiences: []string{"my-api"}, // any-of; empty disables the aud check
        // Leeway, RefreshInterval, AllowedAlgorithms have sane defaults.
    },
    authn.WithOIDCDiscovery("https://issuer.example.com"),
)
if err != nil {
    return err
}

auth, err := gtbhttp.AuthMiddleware(
    gtbhttp.WithBearerVerifier(verifier), // reads "Authorization: Bearer <token>"
)
if err != nil {
    return err
}
http.ListenAndServe(":8080", auth(mux))

The verifier rejects alg:none, rejects HMAC algorithms whenever a JWKS is configured (algorithm-confusion defence), and tolerates small clock skew (Leeway, default 60s). Signing keys are fetched over HTTPS and cached.

Read the caller's identity in a handler

After authentication, the verified *authn.Identity is on the request context:

func handler(w http.ResponseWriter, r *http.Request) {
    id, ok := gtbhttp.IdentityFromContext(r.Context())
    if !ok {
        http.Error(w, "unauthenticated", http.StatusUnauthorized)
        return
    }
    // id.Subject — the principal; id.Method — "apikey" | "jwt" | "mtls"
    // id.Claims  — verified JWT claims (nil for API key); id.Scopes — parsed scopes
    fmt.Fprintf(w, "hello %s\n", id.Subject)
}

Authorize, not just authenticate

Authentication proves who the caller is; authorization decides what they may do. Pass an AuthorizeFunc to gate on claims or scopes — it runs after a successful verify, and a false result yields 403:

requireAdmin := authn.AuthorizeFunc(func(_ context.Context, id *authn.Identity) bool {
    return slices.Contains(id.Scopes, "admin")
})

auth, err := gtbhttp.AuthMiddleware(
    gtbhttp.WithBearerVerifier(verifier),
    gtbhttp.WithAuthorize(requireAdmin),
)

Notes

  • Fail-closed. AuthMiddleware returns an error if you pass no verifier — it never silently allows unauthenticated traffic.
  • Combine methods. Pass several With…Verifier options to accept a bearer token or an API key; the first that matches the request wins.
  • mTLS. For client-certificate auth, use WithMTLSVerifier(authn.NewMTLSVerifier()) alongside TLS-terminating server config.