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-compatible static 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, 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. WithIdentity injects an identity manually, which is useful in tests and custom middleware:
id, ok := authware.IdentityFromContext(r.Context()) ctx := authware.WithIdentity(r.Context(), id)
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 that require RFC 7591 Dynamic Client Registration (e.g. Claude Desktop) with upstream IdPs where the application client is pre-registered out-of-band (e.g. Azure AD, Okta). It provides four handlers: ASMetadataHandler, AuthorizeHandler, RegisterHandler, and TokenHandler. All proxy JSON serialization uses go-jsonfast Builder with pooled buffers.
For confidential-client token exchange (e.g. Azure AD), set OAuthClientSecret in addition to OAuthClientID. The TokenHandler automatically injects client_id and client_secret into the upstream token request, bridging MCP public-client flows with IdPs that require a client secret.
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.
Note: the WWW-Authenticate challenge does not include a resource_metadata parameter (RFC 9728). If your nginx configuration requires it, proxy through a handler that calls auth.Challenge with the full resource metadata URL.
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 via WithIdentity. On failure it writes the WWW-Authenticate challenge header and the appropriate HTTP error status.
Note: the WWW-Authenticate challenge does not include a resource_metadata parameter (RFC 9728) because the server's base URL is not known at middleware construction time. To include resource_metadata, write a custom middleware that calls auth.Challenge with the appropriate URL.
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 is the authentication mode: "none", "bearer", "apikey", or "oauth".
Mode string
// Realm is the realm included in WWW-Authenticate challenges (default: "restricted").
Realm string
// BearerToken is the expected static bearer token (bearer mode only).
BearerToken string
// APIKey is the expected static API key value (apikey mode only).
APIKey string
// APIKeyHeader is the request header that carries the API key (default: "X-API-Key").
APIKeyHeader string
// OAuthIssuer is the token issuer URL; must match the "iss" claim (oauth mode).
OAuthIssuer string
// OAuthAudience is the required audience claim; empty means any audience is accepted.
OAuthAudience string
// OAuthJWKSURL overrides the JWKS endpoint; auto-discovered via OIDC if empty.
OAuthJWKSURL string
// OAuthHMACSecret enables HMAC (HS256/HS384/HS512) validation with a shared secret.
// Intended for testing only; prefer asymmetric keys in production.
OAuthHMACSecret string
// OAuthResource is the protected resource URI served in RFC 9728 metadata.
OAuthResource string
// OAuthResourceDocumentation is the URL of the resource documentation,
// included in RFC 9728 metadata when non-empty.
OAuthResourceDocumentation string
// OAuthResourceName is the human-readable resource name included in RFC 9728 metadata.
OAuthResourceName string
// OAuthClientID is the upstream IdP client_id returned by the built-in DCR shim.
OAuthClientID string
// OAuthClientSecret is the upstream IdP client_secret for confidential-client token exchange.
OAuthClientSecret string
// OAuthRequiredScopes lists the scopes every token must possess.
OAuthRequiredScopes []string
// OAuthAuthorizationServers lists the authorization server URLs advertised in
// RFC 9728 metadata and used for OIDC discovery.
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_CLIENT_ID: upstream IdP client_id for the OAuth proxy DCR shim
- AUTH_OAUTH_CLIENT_SECRET: upstream IdP client_secret (confidential client)
- 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 ¶
type Identity struct {
// Subject is the authenticated principal: the "sub" claim, or "client_id"/"azp" as fallback.
Subject string
// Method is the authentication mode that produced this identity (e.g. ModeOAuth, ModeBearer).
Method string
// Scopes is the space-separated list of OAuth scopes granted to this identity.
Scopes string
}
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, a static client registration shim (RFC 7591-compatible compatibility facade), authorization redirect, and token endpoint proxying. This bridges MCP clients that require public-client OAuth with upstream IdPs where the actual application is pre-registered.
Bridge model: downstream MCP clients are treated as public clients (no credentials); the proxy optionally acts as a confidential backend by injecting client_id and client_secret into upstream token requests when OAuthClientSecret is configured.
Upstream discovery: the authorization and token endpoints are fetched from the upstream IdP's discovery document once, on the first request, using sync.Once. The result is cached for the lifetime of the process. If the upstream IdP rotates its endpoints, the process must be restarted to pick up the new values. This is an intentional trade-off: the vast majority of IdPs (Azure AD, Okta, Google) publish stable, permanent endpoint URLs.
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("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
// Test the DCR shim returns the pre-configured client_id.
r := httptest.NewRequest(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 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 that acts as a static client registration shim compatible with RFC 7591. It always returns the pre-configured upstream IdP client_id without persisting any client metadata.
The redirect_uris field from the request body is echoed back verbatim so that MCP clients know which URI to include in the authorization request. The URIs are not validated here; they must be pre-registered with the upstream IdP out-of-band.
This bridges MCP clients that require RFC 7591 Dynamic Client Registration with upstream IdPs where the actual client is pre-registered out-of-band. The MCP client is treated as a public client (token_endpoint_auth_method=none); the proxy optionally acts as a confidential backend by injecting credentials in TokenHandler when OAuthClientSecret is set.
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.