Documentation
¶
Overview ¶
Package authware provides pluggable HTTP authentication for Go servers.
Five mutually exclusive modes are supported: ModeNone, ModeBearer, ModeAPIKey, ModeOAuth and ModeMTLS. Mode selection is explicit via Config.Mode or inferred from the populated fields.
Middleware authenticates the request, stores the Identity in the request context, and writes a WWW-Authenticate challenge on failure. RequireCapability composes admission predicates over the stored identity; RequireScopes is the scope-only specialisation.
MaxBytes caps inbound body size, SecurityHeaders writes a fixed pre-computed set of response headers, CSRF is a thin wrapper over net/http.CrossOriginProtection. NewRedactor wraps a slog handler to redact sensitive attribute values; RedactHeader does the same in-place on an net/http.Header.
JWT validation auto-discovers the JWKS endpoint from the issuer when Config.OAuthJWKSURL is empty.
AuthCheckHandler is an net/http.Handler compatible with the nginx auth_request module: on success it sets X-Auth-Subject, X-Auth-Method and X-Auth-Scopes response headers.
OAuthProxy bridges clients that require Dynamic Client Registration with upstream IdPs where the application client is pre-registered out-of-band. It exposes OAuthProxy.ASMetadataHandler, OAuthProxy.AuthorizeHandler, OAuthProxy.RegisterHandler and OAuthProxy.TokenHandler.
ConfigFromEnv builds a Config from AUTH_* environment variables.
Index ¶
- func AuthCheckHandler(auth Authenticator) http.Handler
- func CSRF(opts CSRFOptions) (func(http.Handler) http.Handler, error)
- func MaxBytes(maxBytes int64) func(http.Handler) http.Handler
- func Middleware(auth Authenticator) func(http.Handler) http.Handler
- func NewRedactor(inner slog.Handler, keys ...string) slog.Handler
- func RedactHeader(h http.Header, keys ...string) http.Header
- func RequireCapability(checks ...Capability) func(http.Handler) http.Handler
- func RequireScopes(scopes ...string) func(http.Handler) http.Handler
- func SecurityHeaders(cfg *SecureHeadersConfig) func(http.Handler) http.Handler
- func SensitiveHeaders() []string
- func WithIdentity(ctx context.Context, id *Identity) context.Context
- type Authenticator
- type CSRFOptions
- type Capability
- type Config
- type Identity
- func (id *Identity) Claim(name string) (any, bool)
- func (id *Identity) ClaimBool(name string) (value, ok bool)
- func (id *Identity) ClaimFloat64(name string) (float64, bool)
- func (id *Identity) ClaimInt64(name string) (int64, bool)
- func (id *Identity) ClaimString(name string) (string, bool)
- func (id *Identity) RangeClaims(fn func(name string, value any) bool) bool
- type Mode
- type OAuthProxy
- type ProtectedResourceMetadata
- type SecureHeadersConfig
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AuthCheckHandler ¶
func AuthCheckHandler(auth Authenticator) http.Handler
AuthCheckHandler returns an HTTP handler compatible with nginx auth_request. Success returns 200 with X-Auth-Subject, X-Auth-Method and (when non-empty) X-Auth-Scopes. Header values are sanitized against control bytes. Both success and failure responses set Cache-Control: no-store so a caching reverse proxy never serves another caller's identity headers from cache.
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{
Mode: authware.ModeBearer,
BearerToken: "secret",
}, nil)
if err != nil {
log.Fatal(err)
}
handler := authware.AuthCheckHandler(auth)
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/check", http.NoBody)
r.Header.Set("Authorization", "Bearer secret")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code)
fmt.Println(w.Header().Get("X-Auth-Method"))
}
Output: 200 bearer
func CSRF ¶ added in v1.0.0
CSRF wraps net/http.CrossOriginProtection. A malformed entry in TrustedOrigins is reported as an error.
func MaxBytes ¶ added in v1.0.0
MaxBytes limits inbound request body size to maxBytes via http.MaxBytesReader. A non-positive limit installs a passthrough.
func Middleware ¶
func Middleware(auth Authenticator) func(http.Handler) http.Handler
Middleware authenticates every request, stores the Identity in the request context on success, and writes a WWW-Authenticate challenge on failure.
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{
Mode: authware.ModeBearer,
BearerToken: "tok",
}, nil)
if err != nil {
log.Fatal(err)
}
handler := authware.Middleware(auth)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, _ := authware.IdentityFromContext(r.Context())
w.Header().Set("X-Subject", id.Subject)
}))
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
r.Header.Set("Authorization", "Bearer tok")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code)
}
Output: 200
func NewRedactor ¶ added in v1.0.0
NewRedactor wraps inner so any slog attribute whose key matches one of keys (case-insensitive) is replaced with "***". Empty keys returns inner unchanged.
Example ¶
package main
import (
"fmt"
"log/slog"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
h := authware.NewRedactor(slog.NewTextHandler(httptest.NewRecorder(), nil), authware.SensitiveHeaders()...)
logger := slog.New(h)
logger.Info("req", "Authorization", "Bearer secret-value")
fmt.Println("ok")
}
Output: ok
func RedactHeader ¶ added in v1.0.0
RedactHeader replaces in-place every value of every header in h whose key is listed in keys. Empty keys uses the package default set.
func RequireCapability ¶ added in v1.0.0
func RequireCapability(checks ...Capability) func(http.Handler) http.Handler
RequireCapability admits a request only if every check returns true against the identity stored in r.Context. Must be applied after Middleware.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
gate := authware.RequireCapability(
authware.HasMethod(authware.ModeOAuth),
authware.HasAllScopes("read", "write"),
)
handler := gate(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
r = r.WithContext(authware.WithIdentity(r.Context(), &authware.Identity{
Subject: "u", Method: authware.ModeOAuth,
Scopes: []string{"read", "write", "admin"},
}))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code)
}
Output: 200
func RequireScopes ¶
RequireScopes admits a request only if the identity carries every scope listed.
func SecurityHeaders ¶ added in v1.0.0
func SecurityHeaders(cfg *SecureHeadersConfig) func(http.Handler) http.Handler
SecurityHeaders writes a fixed set of response headers derived from cfg. Header values are pre-computed once; the per-request path is allocation-free.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
mw := authware.SecurityHeaders(&authware.SecureHeadersConfig{
HSTSMaxAge: 31536000,
ContentTypeNosniff: true,
FrameOptions: "DENY",
})
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody))
fmt.Println(w.Header().Get("X-Frame-Options"))
}
Output: DENY
func SensitiveHeaders ¶ added in v1.0.0
func SensitiveHeaders() []string
SensitiveHeaders returns a fresh copy of the default redaction set; callers may safely append.
Types ¶
type Authenticator ¶
type Authenticator interface {
Authenticate(r *http.Request) (*Identity, error)
Challenge(err error, resourceMetadataURL string) (status int, header, message string)
Metadata(resource string) *ProtectedResourceMetadata
}
Authenticator validates an inbound HTTP request.
func New ¶
func New(cfg *Config, client *http.Client) (Authenticator, error)
New creates an Authenticator from the provided configuration. client is consulted by ModeOAuth for JWKS fetching; nil installs a default. New does not mutate the caller's Config: a shallow copy is taken before normalisation.
Example (ApiKey) ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{
Mode: authware.ModeAPIKey,
APIKey: "secret-key",
}, nil)
if err != nil {
log.Fatal(err)
}
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
r.Header.Set("X-Api-Key", "secret-key")
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: apikey
Example (Bearer) ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{
Mode: authware.ModeBearer,
BearerToken: "my-secret-token",
}, nil)
if err != nil {
log.Fatal(err)
}
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
r.Header.Set("Authorization", "Bearer my-secret-token")
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: bearer
Example (MTLS) ¶
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
cert := &x509.Certificate{Subject: pkix.Name{CommonName: "client.example"}}
auth, err := authware.New(&authware.Config{
Mode: authware.ModeMTLS,
MTLSAllowedSubjects: []string{"client.example"},
}, nil)
if err != nil {
log.Fatal(err)
}
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
// Subject matching requires a chain verified by the TLS layer
// (ClientAuth: tls.RequireAndVerifyClientCert).
r.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
VerifiedChains: [][]*x509.Certificate{{cert}},
}
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method, id.Subject)
}
Output: mtls client.example
Example (None) ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{Mode: authware.ModeNone}, nil)
if err != nil {
log.Fatal(err)
}
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: none
type CSRFOptions ¶ added in v1.0.0
CSRFOptions configures the CSRF middleware.
type Capability ¶ added in v1.0.0
Capability is a predicate over an authenticated Identity.
func HasAllScopes ¶ added in v1.0.0
func HasAllScopes(scopes ...string) Capability
HasAllScopes matches when every scope is present.
func HasAnyScope ¶ added in v1.0.0
func HasAnyScope(scopes ...string) Capability
HasAnyScope matches when at least one scope is present.
func HasClaim ¶ added in v1.0.0
func HasClaim(name string, value any) Capability
HasClaim matches when id.Claim(name) equals value.
func HasMethod ¶ added in v1.0.0
func HasMethod(m Mode) Capability
HasMethod matches when id.Method equals m.
func HasScope ¶ added in v1.0.0
func HasScope(scope string) Capability
HasScope matches when scope is present in id.Scopes.
func HasSubject ¶ added in v1.0.0
func HasSubject(subject string) Capability
HasSubject matches when id.Subject equals subject.
type Config ¶
type Config struct {
OAuthHTTPClient *http.Client
OAuthJWKSURL string
OAuthHMACSecret string
OAuthClientSecret string
OAuthAudience string
Mode Mode
Realm string
OAuthResourceName string
APIKey string
APIKeyHeader string
OAuthIssuer string
OAuthResourceDocumentation string
BearerToken string
OAuthClientID string
OAuthResource string
OAuthRequiredScopes []string
OAuthAuthorizationServers []string
MTLSAllowedSubjects []string
MTLSAllowedSPKIPins [][]byte
SecureCSRFTrusted []string
SecureHeaders SecureHeadersConfig
OAuthClockSkewTolerance time.Duration
OAuthJWKSCacheTTL time.Duration
SecureMaxRequestBytes int64
OAuthProxyFetchTimeout time.Duration
}
Config selects the authentication mode and carries its parameters.
func ConfigFromEnv ¶
func ConfigFromEnv() *Config
ConfigFromEnv reads AUTH_* environment variables into a Config.
Recognized variables:
AUTH_MODE none|bearer|apikey|oauth|mtls AUTH_REALM realm for WWW-Authenticate AUTH_BEARER_TOKEN static bearer token AUTH_API_KEY static API key AUTH_API_KEY_HEADER custom header name (default X-Api-Key) AUTH_OAUTH_ISSUER OAuth issuer URL AUTH_OAUTH_AUDIENCE expected audience claim AUTH_OAUTH_JWKS_URL JWKS endpoint (auto-discovered if empty) AUTH_OAUTH_HMAC_SECRET HMAC shared secret (testing only) AUTH_OAUTH_REQUIRED_SCOPES comma-separated required scopes AUTH_OAUTH_RESOURCE protected resource identifier AUTH_OAUTH_RESOURCE_DOCUMENTATION resource documentation URL AUTH_OAUTH_RESOURCE_NAME human-readable resource name AUTH_OAUTH_CLIENT_ID upstream client_id (proxy DCR shim) AUTH_OAUTH_CLIENT_SECRET upstream client_secret AUTH_OAUTH_AUTHORIZATION_SERVERS comma-separated AS URLs AUTH_MTLS_ALLOWED_SUBJECTS comma-separated CNs AUTH_MTLS_SPKI_PINS comma-separated base64 SHA-256 pins AUTH_SECURE_MAX_BYTES int64 request body cap AUTH_SECURE_HSTS_MAX_AGE HSTS max-age (seconds) AUTH_SECURE_HSTS_SUBS bool, includeSubDomains AUTH_SECURE_HSTS_PRELOAD bool, preload AUTH_SECURE_CSP Content-Security-Policy AUTH_SECURE_FRAME_OPTIONS DENY|SAMEORIGIN AUTH_SECURE_NOSNIFF bool, X-Content-Type-Options AUTH_SECURE_REFERRER_POLICY Referrer-Policy AUTH_SECURE_PERMISSIONS_POLICY Permissions-Policy AUTH_SECURE_XSS_PROTECTION X-XSS-Protection AUTH_SECURE_CSRF_TRUSTED comma-separated trusted origins
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
cfg := authware.ConfigFromEnv()
auth, err := authware.New(cfg, nil)
if err != nil {
log.Fatal(err)
}
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: none
type Identity ¶
type Identity struct {
PeerCert *x509.Certificate
Subject string
Method Mode
Scopes []string
// contains filtered or unexported fields
}
Identity describes the authenticated caller. Static modes return a shared singleton; OAuth and mTLS allocate per request. Identity must be treated as read-only.
func IdentityFromContext ¶
IdentityFromContext returns the authenticated identity stored in ctx by Middleware. The returned pointer is read-only.
Example ¶
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
auth, err := authware.New(&authware.Config{
Mode: authware.ModeBearer,
BearerToken: "tok",
}, nil)
if err != nil {
log.Fatal(err)
}
handler := authware.Middleware(auth)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
id, ok := authware.IdentityFromContext(r.Context())
fmt.Println(ok, id.Method)
}))
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", http.NoBody)
r.Header.Set("Authorization", "Bearer tok")
handler.ServeHTTP(httptest.NewRecorder(), r)
}
Output: true bearer
func (*Identity) Claim ¶ added in v1.0.0
Claim returns the named JWT claim decoded into a Go value: string, int64, float64, bool, nil, or the raw JSON for objects/arrays.
func (*Identity) ClaimFloat64 ¶ added in v1.0.0
ClaimFloat64 returns the named numeric claim as a float.
func (*Identity) ClaimInt64 ¶ added in v1.0.0
ClaimInt64 returns the named integer claim.
func (*Identity) ClaimString ¶ added in v1.0.0
ClaimString returns the named string claim.
type OAuthProxy ¶ added in v0.1.0
type OAuthProxy struct {
// contains filtered or unexported fields
}
OAuthProxy provides HTTP handlers for the OAuth facade. Upstream discovery is lazy and single-flight: the elected goroutine never holds a mutex while the upstream HTTP call runs.
func NewOAuthProxy ¶ added in v0.1.0
func NewOAuthProxy(cfg *Config, log *slog.Logger) *OAuthProxy
NewOAuthProxy creates an OAuth proxy from the given Config. Returns nil when no AuthorizationServers are configured or no ClientID is set (proxy not needed).
cfg.OAuthHTTPClient is used for upstream calls when set; nil installs a fresh client with the configured fetch timeout. The fetch timeout falls back to 10s when cfg.OAuthProxyFetchTimeout is zero.
Example ¶
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"github.com/ubyte-source/go-authware"
)
func main() {
proxy := authware.NewOAuthProxy(&authware.Config{
OAuthAuthorizationServers: []string{"https://login.microsoftonline.com/tenant/v2.0"},
OAuthClientID: "my-client-id",
}, slog.Default())
if proxy != nil {
mux := http.NewServeMux()
mux.HandleFunc("GET /.well-known/oauth-authorization-server", proxy.ASMetadataHandler())
mux.HandleFunc("GET /authorize", proxy.AuthorizeHandler())
mux.HandleFunc("POST /register", proxy.RegisterHandler())
mux.HandleFunc("POST /token", proxy.TokenHandler())
_ = mux
r := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/register", http.NoBody)
w := httptest.NewRecorder()
proxy.RegisterHandler().ServeHTTP(w, r)
fmt.Println(w.Code)
}
}
Output: 201
func (*OAuthProxy) ASMetadataHandler ¶ added in v0.1.0
func (p *OAuthProxy) ASMetadataHandler() http.HandlerFunc
ASMetadataHandler serves the AS metadata document.
func (*OAuthProxy) AuthorizeHandler ¶ added in v0.2.0
func (p *OAuthProxy) AuthorizeHandler() http.HandlerFunc
AuthorizeHandler redirects to the upstream IdP authorisation endpoint.
func (*OAuthProxy) RegisterHandler ¶ added in v0.1.0
func (p *OAuthProxy) RegisterHandler() http.HandlerFunc
RegisterHandler returns a static client registration shim.
func (*OAuthProxy) TokenHandler ¶ added in v0.1.0
func (p *OAuthProxy) TokenHandler() http.HandlerFunc
TokenHandler proxies token exchange to the upstream IdP token endpoint.
type ProtectedResourceMetadata ¶
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
ResourceDocumentation string `json:"resource_documentation,omitempty"`
ResourceName string `json:"resource_name,omitempty"`
AuthorizationServers []string `json:"authorization_servers,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
}
ProtectedResourceMetadata is the Protected Resource Metadata document served by [oauthAuthenticator.Metadata].
type SecureHeadersConfig ¶ added in v1.0.0
type SecureHeadersConfig struct {
CSP string
FrameOptions string
ReferrerPolicy string
PermissionsPolicy string
XSSProtection string
HSTSMaxAge int
HSTSIncludeSubs bool
HSTSPreload bool
ContentTypeNosniff bool
}
SecureHeadersConfig configures the SecurityHeaders middleware.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cred provides outbound HTTP credentials.
|
Package cred provides outbound HTTP credentials. |
|
Package replay implements anti-replay HMAC for HTTP requests.
|
Package replay implements anti-replay HMAC for HTTP requests. |
|
Package secret provides a small interface for fetching credentials from a backing store plus three built-in providers (Static, Env, File) and a per-tenant Resolver.
|
Package secret provides a small interface for fetching credentials from a backing store plus three built-in providers (Static, Env, File) and a per-tenant Resolver. |