authware

package module
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 30 Imported by: 0

README

go-authware

Pluggable HTTP authentication, outbound credentials, secret management, and anti-replay for Go services. Stdlib + go-jsonfast. Nothing else.

Go Version Lint Test Security Fuzz Go Report Card Go Reference

Packages

Import path Purpose
github.com/ubyte-source/go-authware Server-side authentication (Bearer, API Key, OAuth/JWT, mTLS, OAuth proxy, hardening middleware)
github.com/ubyte-source/go-authware/cred Outbound credentials (OAuth2, AWS SigV4, Azure MSI/SPN, GCP, mTLS, Cache, RoundTripper)
github.com/ubyte-source/go-authware/secret Secret providers (Static, Env, File) and per-tenant Resolver
github.com/ubyte-source/go-authware/replay HMAC anti-replay envelope (Signer, Verifier, NonceStore, Memory)

The four packages are independent. Import only what you need.

Install

go get github.com/ubyte-source/go-authware

Server-side authentication

Five mutually exclusive modes, selected by Config.Mode or inferred from the fields you populate:

auth, err := authware.New(&authware.Config{
    Mode:        authware.ModeBearer,
    BearerToken: "my-secret-token",
}, nil)
if err != nil { log.Fatal(err) }

mux := http.NewServeMux()
mux.Handle("/api/", authware.Middleware(auth)(apiHandler))

The authenticated identity is stored in the request context as a read-only *Identity:

id, ok := authware.IdentityFromContext(r.Context())
// id.Subject, id.Method, id.Scopes, id.PeerCert
// id.Claim(name) for OAuth claims (lazy decode, zero alloc on miss)
// id.RangeClaims(fn) to walk every claim

JWT claims are not eagerly decoded into a map[string]any. Use id.Claim(name) for the common single-claim case (zero alloc on miss, one parse on hit) or id.RangeClaims(fn) to iterate the payload.

Mode reference
Mode Required fields Notes
ModeNone Pass-through; useful for tests and dev
ModeBearer BearerToken Constant-time comparison
ModeAPIKey APIKey, optional APIKeyHeader Custom header or Authorization: ApiKey <key>
ModeOAuth OAuthIssuer (+ optional fields) JWT validation with JWKS / OIDC discovery
ModeMTLS MTLSAllowedSubjects and/or MTLSAllowedSPKIPins Verifies peer certificate against an allowlist
Capability-based authorization

RequireCapability composes admission predicates over the authenticated identity. RequireScopes is a thin wrapper for the common case.

mux.Handle("/admin/", authware.Middleware(auth)(
    authware.RequireCapability(
        authware.HasMethod(authware.ModeOAuth),
        authware.HasAllScopes("admin"),
        authware.HasClaim("tenant", "acme"),
    )(adminHandler),
))

Built-in predicates: HasScope, HasAnyScope, HasAllScopes, HasClaim, HasMethod, HasSubject. Custom predicates are any func(*Identity) bool.

OIDC auto-discovery

Omit OAuthJWKSURL and the library fetches {issuer}/.well-known/openid-configuration once, caching the JWKS with TTL and stampede-prevented refresh. A token carrying a kid absent from the cache forces one rate-limited refetch, so IdP key rotation is picked up without waiting for TTL expiry. Works with Google, Auth0, Keycloak, Azure AD, Okta, and any other OIDC-compliant provider.

auth, _ := authware.New(&authware.Config{
    Mode:                authware.ModeOAuth,
    OAuthIssuer:         "https://accounts.google.com",
    OAuthAudience:       "my-api",
    OAuthRequiredScopes: []string{"api:read"},
}, nil)

The issuer URL and the JWKS URL it advertises must use https://. Plain http:// is accepted only for loopback hosts (localhost, 127.0.0.1, ::1) so that test fixtures keep working. RSA keys with a modulus shorter than 2048 bits are rejected (NIST SP 800-131A Rev. 2).

Server mTLS
auth, _ := authware.New(&authware.Config{
    Mode:                authware.ModeMTLS,
    MTLSAllowedSubjects: []string{"client.example"},
    MTLSAllowedSPKIPins: [][]byte{spkiSHA256Pin},
}, nil)

The verified *x509.Certificate is exposed as Identity.PeerCert. Pin matching uses subtle.ConstantTimeCompare on the SHA-256 SPKI digest. Subject matching requires a chain verified by the TLS layer (ClientAuth: tls.RequireAndVerifyClientCert): any key holder can self-sign an arbitrary subject. SPKI pins bind the key itself and also accept unverified chains.

Hardening middleware
maxBytes := authware.MaxBytes(1 << 20)                 // 1 MiB body cap
sec      := authware.SecurityHeaders(&authware.SecureHeadersConfig{
    HSTSMaxAge:         31536000,
    HSTSIncludeSubs:    true,
    ContentTypeNosniff: true,
    FrameOptions:       "DENY",
    ReferrerPolicy:     "no-referrer",
})
csrf, err := authware.CSRF(authware.CSRFOptions{
    TrustedOrigins: []string{"https://app.example"},
    BypassPaths:    []string{"/api/webhook"},
})

SecurityHeaders precomputes the (name, value) pairs once and writes them via direct http.Header map assignment — zero-allocation per request. CSRF is a thin shim over http.NewCrossOriginProtection (Go 1.25+).

Log redaction
handler := authware.NewRedactor(slog.NewJSONHandler(os.Stdout, nil),
    authware.SensitiveHeaders...)
slog.SetDefault(slog.New(handler))

// In an http handler before logging request headers:
authware.RedactHeader(r.Header.Clone())

NewRedactor walks slog records (and grouped attributes) replacing matched keys with ***. RedactHeader mutates an http.Header in place.

OAuth proxy (MCP 2025-11-25)

OAuthProxy bridges public-client MCP flows with upstream IdPs that don't natively support RFC 7591 Dynamic Client Registration:

proxy := authware.NewOAuthProxy(&authware.Config{
    OAuthAuthorizationServers: []string{"https://login.microsoftonline.com/tenant/v2.0"},
    OAuthClientID:             "my-app-client-id",
    OAuthClientSecret:         "my-app-client-secret",
}, slog.Default())

if proxy != nil {
    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())
}
Nginx auth_request

AuthCheckHandler returns an http.Handler compatible with the nginx auth_request module. On success it returns 200 with X-Auth-Subject, X-Auth-Method, and X-Auth-Scopes response headers. Header values are sanitised against CR/LF/control bytes (CWE-113) so a JWT-derived subject cannot inject additional response headers.

mux.Handle("/check", authware.AuthCheckHandler(auth))
Environment configuration

ConfigFromEnv reads the AUTH_* environment variables. See .env.example for the full list (auth modes, OAuth, mTLS, hardening headers, CSRF).

cfg := authware.ConfigFromEnv()
auth, err := authware.New(cfg, nil)

Outbound credentials — cred

import "github.com/ubyte-source/go-authware/cred"
Symbol Role
Token Outbound credential; Apply writes it to *http.Request
TokenSource, TokenSourceFunc Produces tokens; safe for concurrent use
Signer, SignerFunc Signs a request in place (for SigV4-style schemes)
AsSigner(TokenSource) Signer Adapts a token source to a signer
Cache(TokenSource, ...) TTL caching with manual singleflight; zero-alloc hit path
RoundTripper(base, Signer) http.RoundTripper that clones the request and signs it
ClientCredentials OAuth2 client_credentials grant (RFC 6749 §4.4)
AuthorizationCode OAuth2 refresh_token grant with rotation
SigV4 AWS Signature Version 4 (canonical form, no SDK)
AzureMSI Azure Instance Metadata Service (managed identity)
AzureSPN Azure AD service principal via client_credentials
GCPMetadata GCP metadata server (access token or audience-bound ID token)
LoadClientTLS, ReloadingClientTLS Outbound TLS configuration with on-demand reload

End-to-end flow:

src := &cred.ClientCredentials{
    TokenURL: "https://issuer.example/oauth2/token",
    ClientID: "id", ClientSecret: "secret",
    Scopes: []string{"api:read"},
}
cached := cred.Cache(src)                          // TTL + singleflight
client := &http.Client{
    Transport: cred.RoundTripper(nil, cred.AsSigner(cached)),
}

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example/v1/items", nil)
resp, err := client.Do(req)                        // Authorization auto-attached

AWS SigV4:

signer := &cred.SigV4{
    AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
    SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
    Region:    "us-east-1",
    Service:   "execute-api",
}
client := &http.Client{Transport: cred.RoundTripper(nil, signer)}

The package validates against the AWS-published get-vanilla test vector byte-for-byte.

Secret management — secret

import "github.com/ubyte-source/go-authware/secret"
p := secret.Static(map[string]string{"db_password": "p4ss"})
v, _ := p.Secret(ctx, "db_password")     // → "p4ss"

Three providers shipped:

  • secret.Static(map) — in-memory.
  • secret.Env(prefix) — environment variables under prefix.
  • secret.File(path) — flat JSON document loaded once.

Per-tenant resolution:

r := secret.MapResolver(map[string]secret.Provider{
    "alpha": secret.Static(...),
    "beta":  secret.Env("BETA_"),
}, secret.Static(defaults))
v, _ := r.For(tenantID).Secret(ctx, "db_password")

Vault, AWS Secrets Manager, GCP Secret Manager, and SOPS adapters are intentionally excluded. Each is a 30-line wrapper around secret.Provider that you add to your own binary, keeping go-authware dependency-free.

Anti-replay HMAC — replay

import "github.com/ubyte-source/go-authware/replay"

Three headers carry the envelope: X-Auth-Timestamp, X-Auth-Nonce, X-Auth-Signature. Signed string is method "\n" path "\n" timestamp "\n" nonce (body is not part of the signature; use cred.SigV4 if you need body binding).

Server side:

v := &replay.Verifier{
    Key:        sharedKey,                // ≥ 32 bytes
    Window:     5 * time.Minute,
    NonceStore: replay.Memory(8192),
}
mux.Handle("/api/", replay.Middleware(v)(apiHandler))

Client side:

s := &replay.Signer{Key: sharedKey}
client := &http.Client{Transport: cred.RoundTripper(nil, s)}

Memory is a TTL store suitable for single-instance deployments; full of live nonces it fails closed with ErrStoreFull — size its capacity above peak signed requests per second × twice the verifier window. For fleets, plug in any NonceStore implementation (Redis, etc.).

Benchmarks

Measured on Intel Xeon Gold 6426Y. Hot paths designed for zero allocation are achieved.

Server authentication (go-authware root)
Benchmark ns/op B/op allocs/op Notes
None 1.5 0 0 Pre-built shared *Identity
Bearer 16 0 0 Constant-time compare; shared identity
API Key (header) 22 0 0
API Key (Authorization) 36 0 0
HasRequiredScopes 10 0 0 slices.Contains
RequireCapability 29 0 0 Pointer-in-context lookup
MTLS accept (subject) 43 80 1 Identity per request
MTLS accept (pin) 192 80 1 SHA-256 SPKI compare
SecurityHeaders 70 0 0 Pre-computed headerKV slice
MaxBytes 80 64 1 Inherent http.MaxBytesReader alloc
Middleware (Bearer) 136 368 2 r.WithContext + ctx node
AuthCheckHandler 137 32 2 nginx auth_request handler
OAuth HMAC HS256 1432 289 5 jsonfast claims, lazy decode
RedactHeader 217 48 3 Pre-canonicalised sensitive set
Redactor (slog hit) 1007 16 1 Recursive group walk
Outbound credentials (cred)
Benchmark ns/op B/op allocs/op Notes
Cache hit 77 0 0 atomic.Pointer.Load + freshness
Token.Apply 118 40 2 inherent http.Header.Set
RoundTripper 572 1160 9 dominated by r.Clone(ctx)
ParseTokenResponse 294 88 4 jsonfast iteration
SigV4 sign 8282 4944 62 HMAC chain + canonical encoding
Anti-replay (replay)
Benchmark ns/op B/op allocs/op Notes
Memory.Seen 362 71 1 Typed doubly-linked TTL list
Signer.Sign 2257 1538 20 Pooled scratch + 3 header sets
Verify.Hit 4334 2145 32 Sign + Verify together
Secrets (secret)
Benchmark ns/op B/op allocs/op Notes
Static.Hit 8.4 0 0 map lookup
Env.Hit 112 32 2 strings.ToUpper + os.LookupEnv

Project layout

go-authware/
├── *.go ↔ *_test.go                # Server-side auth, hardening, OAuth proxy
├── doc.go example_test.go
├── cred/                           # Outbound credentials
│   ├── *.go ↔ *_test.go
│   └── doc.go example_test.go
├── secret/                         # Secret providers
│   ├── secret.go ↔ secret_test.go
│   └── doc.go example_test.go
└── replay/                         # Anti-replay HMAC
    ├── replay.go ↔ replay_test.go
    └── doc.go example_test.go

Every production .go file has a paired <name>_test.go so the test for any unit of code is easy to find.

Security

The threat model, implemented defenses, operational limits, and the continuous-verification gates (go vet, -race, golangci-lint including gosec, govulncheck, native fuzz suite) are documented in SECURITY.md. Vulnerability reports go through GitHub's private reporting flow described there.

Dependencies

github.com/ubyte-source/go-jsonfast

That's it. No golang.org/x/oauth2, no AWS SDK, no Azure/GCP SDKs, no Vault adapters. Each provider is implemented directly against the relevant RFC or vendor metadata endpoint.

License

MIT — see LICENSE.

Authors

Support

If go-authware is useful for your services, consider supporting the work:

Buy Me A Coffee

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

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

func CSRF(opts CSRFOptions) (func(http.Handler) http.Handler, error)

CSRF wraps net/http.CrossOriginProtection. A malformed entry in TrustedOrigins is reported as an error.

func MaxBytes added in v1.0.0

func MaxBytes(maxBytes int64) func(http.Handler) http.Handler

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

func NewRedactor(inner slog.Handler, keys ...string) slog.Handler

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

func RedactHeader(h http.Header, keys ...string) http.Header

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

func RequireScopes(scopes ...string) func(http.Handler) http.Handler

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.

func WithIdentity

func WithIdentity(ctx context.Context, id *Identity) context.Context

WithIdentity returns a copy of ctx carrying id.

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

type CSRFOptions struct {
	DenyHandler    http.Handler
	TrustedOrigins []string
	BypassPaths    []string
}

CSRFOptions configures the CSRF middleware.

type Capability added in v1.0.0

type Capability func(*Identity) bool

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

func IdentityFromContext(ctx context.Context) (*Identity, bool)

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

func (id *Identity) Claim(name string) (any, bool)

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) ClaimBool added in v1.0.0

func (id *Identity) ClaimBool(name string) (value, ok bool)

ClaimBool returns the named boolean claim.

func (*Identity) ClaimFloat64 added in v1.0.0

func (id *Identity) ClaimFloat64(name string) (float64, bool)

ClaimFloat64 returns the named numeric claim as a float.

func (*Identity) ClaimInt64 added in v1.0.0

func (id *Identity) ClaimInt64(name string) (int64, bool)

ClaimInt64 returns the named integer claim.

func (*Identity) ClaimString added in v1.0.0

func (id *Identity) ClaimString(name string) (string, bool)

ClaimString returns the named string claim.

func (*Identity) RangeClaims added in v1.0.0

func (id *Identity) RangeClaims(fn func(name string, value any) bool) bool

RangeClaims iterates every top-level JWT claim. The name is valid only for the duration of the callback; clone via strings.Clone to retain.

type Mode added in v1.0.0

type Mode string

Mode names a supported HTTP authentication scheme.

const (
	ModeNone   Mode = "none"
	ModeBearer Mode = "bearer"
	ModeAPIKey Mode = "apikey"
	ModeOAuth  Mode = "oauth"
	ModeMTLS   Mode = "mtls"
)

Supported authentication modes.

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL