Documentation
¶
Overview ¶
Package authware provides pluggable HTTP authentication for Go servers.
Designed for high-throughput production services where authentication must be both correct and fast. Supports four modes out of the box:
- None — pass-through, no authentication
- Bearer — static token comparison (constant-time)
- API Key — header or Authorization scheme comparison
- OAuth / JWT — full JWT validation with JWKS auto-discovery
All static comparisons use crypto/subtle.ConstantTimeCompare. JWT validation uses zero-allocation unsafe string→[]byte views for token parsing and single-pass claims extraction via go-jsonfast.
The OAuth mode implements:
- JWT signature verification (HMAC-SHA256/384/512, RSA, RSA-PSS, ECDSA)
- OIDC auto-discovery from any OpenID Connect provider
- Automatic JWKS key rotation with cache TTL
- Stampede prevention via serialized refresh with double-check
- RFC 9728 Protected Resource Metadata
- RFC 8414 AS Metadata via OAuth proxy
- RFC 7591 Dynamic Client Registration shim
- Scope validation (scope and scp claims)
- Clock skew tolerance for nbf/iat claims
OIDC Auto-Discovery ¶
When OAuthJWKSURL is not configured, the library automatically discovers the JWKS endpoint by fetching {issuer}/.well-known/openid-configuration. This works with any OIDC-compliant provider: Google, Auth0, Keycloak, Azure AD, Okta, Anthropic, and others.
Middleware ¶
The package provides standard HTTP middleware for common auth patterns:
auth, _ := authware.New(cfg, nil)
mux := http.NewServeMux()
mux.Handle("/api/", authware.Middleware(auth)(apiHandler))
mux.Handle("/admin/", authware.Middleware(auth)(
authware.RequireScopes("admin")(adminHandler),
))
The authenticated identity is stored in the request context and can be retrieved via IdentityFromContext:
id := authware.IdentityFromContext(r.Context())
Nginx auth_request ¶
AuthCheckHandler returns an http.Handler compatible with nginx's auth_request module. On success it sets X-Auth-Subject, X-Auth-Method, and X-Auth-Scopes response headers for the upstream:
mux.Handle("/check", authware.AuthCheckHandler(auth))
Environment Configuration ¶
ConfigFromEnv reads AUTH_* environment variables to build a Config, making it easy to configure the provider without code changes:
cfg := authware.ConfigFromEnv() auth, err := authware.New(cfg, nil)
Direct Usage ¶
auth, err := authware.New(cfg, nil) id, err := auth.Authenticate(r)
OAuth Proxy ¶
OAuthProxy bridges MCP clients (e.g. Claude Desktop) with upstream IdPs that don't support RFC 7591 Dynamic Client Registration. It provides four handlers: ASMetadataHandler, AuthorizeHandler, RegisterHandler, and TokenHandler. All proxy JSON serialization uses go-jsonfast Builder with pooled buffers.
proxy := authware.NewOAuthProxy(cfg, slog.Default())
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())
Dependencies ¶
This library depends only on go-jsonfast (https://github.com/ubyte-source/go-jsonfast) for zero-allocation JWT claims parsing and proxy JSON serialization. No other external modules are required.
Index ¶
- Constants
- func AuthCheckHandler(auth Authenticator) http.Handler
- func Middleware(auth Authenticator) func(http.Handler) http.Handler
- func RequireScopes(scopes ...string) func(http.Handler) http.Handler
- func WithIdentity(ctx context.Context, id Identity) context.Context
- type Authenticator
- type Config
- type Identity
- type OAuthProxy
- type ProtectedResourceMetadata
Examples ¶
Constants ¶
const ( ModeNone = "none" ModeBearer = "bearer" ModeAPIKey = "apikey" ModeOAuth = "oauth" )
Supported authentication modes.
Variables ¶
This section is empty.
Functions ¶
func AuthCheckHandler ¶
func AuthCheckHandler(auth Authenticator) http.Handler
AuthCheckHandler returns an HTTP handler for use with nginx auth_request or similar reverse-proxy authentication subrequests.
On success it returns 200 with the following response headers:
- X-Auth-Subject: the authenticated subject
- X-Auth-Method: the authentication method used
- X-Auth-Scopes: space-separated scopes (if any)
On failure it returns the appropriate HTTP status (401/403) with a WWW-Authenticate header for Bearer-based schemes.
Nginx configuration example:
location /api {
auth_request /check;
auth_request_set $auth_user $upstream_http_x_auth_subject;
}
location = /check {
internal;
proxy_pass http://authcheck:9090/check;
proxy_pass_request_headers on;
}
Example ¶
package main
import (
"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.NewRequest(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 Middleware ¶
func Middleware(auth Authenticator) func(http.Handler) http.Handler
Middleware returns an HTTP middleware that authenticates every request. On success the Identity is stored in the request context; on failure the appropriate HTTP status and challenge header are returned.
Example ¶
package main
import (
"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.NewRequest(http.MethodGet, "/", http.NoBody)
r.Header.Set("Authorization", "Bearer tok")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code)
}
Output: 200
func RequireScopes ¶
RequireScopes returns an HTTP middleware that verifies the authenticated identity (from context) possesses all the specified scopes. Must be applied after Middleware.
Types ¶
type Authenticator ¶
type Authenticator interface {
// Authenticate returns the authenticated identity or an error.
Authenticate(r *http.Request) (Identity, error)
// Challenge returns the HTTP status, WWW-Authenticate header, and error message.
Challenge(err error, resourceMetadataURL string) (status int, header string, message string)
// Metadata returns RFC 9728 Protected Resource Metadata, or nil if not applicable.
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.
Example (ApiKey) ¶
package main
import (
"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.NewRequest(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 (
"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.NewRequest(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 (None) ¶
package main
import (
"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.NewRequest(http.MethodGet, "/", http.NoBody)
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: none
type Config ¶
type Config struct {
Mode string
Realm string
BearerToken string
APIKey string
APIKeyHeader string
OAuthIssuer string
OAuthAudience string
OAuthJWKSURL string
OAuthHMACSecret string
OAuthResource string
OAuthResourceDocumentation string
OAuthResourceName string
OAuthClientID string // upstream IdP client_id returned by the built-in DCR shim
OAuthRequiredScopes []string
OAuthAuthorizationServers []string
}
Config defines the supported HTTP authentication modes.
func ConfigFromEnv ¶
func ConfigFromEnv() *Config
ConfigFromEnv reads authentication configuration from environment variables. This is intended for building standalone auth-check servers or configuring authentication in containerized deployments.
Environment variables:
- AUTH_MODE: none|bearer|apikey|oauth
- AUTH_REALM: realm for WWW-Authenticate header (default: restricted)
- AUTH_BEARER_TOKEN: static bearer token
- AUTH_API_KEY: static API key
- AUTH_API_KEY_HEADER: custom header name for API key (default: X-API-Key)
- AUTH_OAUTH_ISSUER: OAuth issuer URL
- AUTH_OAUTH_AUDIENCE: expected audience claim
- AUTH_OAUTH_JWKS_URL: JWKS endpoint (auto-discovered via OIDC 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 (RFC 9728)
- AUTH_OAUTH_RESOURCE_DOCUMENTATION: resource documentation URL
- AUTH_OAUTH_RESOURCE_NAME: human-readable resource name
- AUTH_OAUTH_AUTHORIZATION_SERVERS: comma-separated authorization server URLs
Example ¶
package main
import (
"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.NewRequest(http.MethodGet, "/", http.NoBody)
id, err := auth.Authenticate(r)
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Method)
}
Output: none
type Identity ¶
Identity describes the authenticated caller.
func IdentityFromContext ¶
IdentityFromContext returns the authenticated identity from the request context. Returns the zero value and false if the request was not authenticated.
Example ¶
package main
import (
"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.NewRequest(http.MethodGet, "/", http.NoBody)
r.Header.Set("Authorization", "Bearer tok")
handler.ServeHTTP(httptest.NewRecorder(), r)
}
Output: true bearer
type OAuthProxy ¶ added in v0.1.0
type OAuthProxy struct {
// contains filtered or unexported fields
}
OAuthProxy provides HTTP handlers for the MCP 2025-11-25 OAuth facade. It handles AS metadata discovery, Dynamic Client Registration (DCR) shim, authorization redirect, and token endpoint proxying. This bridges MCP clients that require public-client OAuth with upstream IdPs that don't natively support RFC 7591 DCR.
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 if no OAuthAuthorizationServers are configured or OAuthClientID is empty (proxy not needed).
Example ¶
package main
import (
"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("/.well-known/oauth-authorization-server", proxy.ASMetadataHandler())
mux.HandleFunc("/oauth/register", proxy.RegisterHandler())
mux.HandleFunc("/oauth/token", proxy.TokenHandler())
// Test the DCR shim returns the pre-configured client_id.
r := httptest.NewRequest(http.MethodPost, "/oauth/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 returns an http.HandlerFunc that serves /.well-known/oauth-authorization-server per RFC 8414 and the MCP specification (2025-11-25). It fetches the upstream IdP's discovery document once, then serves a metadata document where the MCP server acts as the authorization server (issuer matches the request origin).
func (*OAuthProxy) AuthorizeHandler ¶ added in v0.2.0
func (p *OAuthProxy) AuthorizeHandler() http.HandlerFunc
AuthorizeHandler returns an http.HandlerFunc that redirects the user to the upstream IdP's authorization endpoint, passing all query parameters through. Per the MCP specification (2025-11-25), clients use /authorize either from AS metadata or as the default fallback. This handler 302-redirects to the real upstream IdP authorize URL.
func (*OAuthProxy) RegisterHandler ¶ added in v0.1.0
func (p *OAuthProxy) RegisterHandler() http.HandlerFunc
RegisterHandler returns an http.HandlerFunc implementing a minimal RFC 7591 Dynamic Client Registration shim. It accepts any registration request and returns the pre-configured upstream IdP client_id.
func (*OAuthProxy) TokenHandler ¶ added in v0.1.0
func (p *OAuthProxy) TokenHandler() http.HandlerFunc
TokenHandler returns an http.HandlerFunc that proxies token exchange requests to the upstream IdP's 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 served from RFC 9728 metadata discovery endpoints.