authware

package module
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Apr 15, 2026 License: MIT Imports: 27 Imported by: 0

README

go-authware

Minimal-dependency HTTP authentication library for Go servers.

Go Version CI Go Report Card Go Reference

Features

  • Five auth modes: None, Bearer, API Key, OAuth/JWT, OAuth Proxy
  • OIDC auto-discovery — works with any OpenID Connect provider
  • Minimal dependencies — stdlib + go-jsonfast for zero-alloc JSON
  • Constant-time comparisons via crypto/subtle on all secret paths
  • JWT/JWKS support: HMAC-SHA256/384/512, RSA (PKCS1v15 + PSS), ECDSA (P-256/384/521)
  • JWKS auto-refresh with cache TTL and stampede prevention
  • RFC 9728 Protected Resource Metadata
  • RFC 8414 AS Metadata via OAuth proxy
  • RFC 7591 Dynamic Client Registration shim
  • Zero-alloc hot paths using unsafe.Slice for string→[]byte views
  • go-jsonfast Builder for zero-alloc JSON serialization in proxy handlers
  • Nginx auth_request compatible handler
  • Environment variable configuration via ConfigFromEnv
  • PGO-optimized with default.pgo profile

Install

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

Quick Start

package main

import (
    "fmt"
    "log"
    "net/http"

    "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)
    }

    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        id, err := auth.Authenticate(r)
        if err != nil {
            status, header, msg := auth.Challenge(err, "")
            if header != "" {
                w.Header().Set("WWW-Authenticate", header)
            }
            http.Error(w, msg, status)
            return
        }
        fmt.Fprintf(w, "Hello, %s (method: %s)", id.Subject, id.Method)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Middleware

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 available in the request context:

id, ok := authware.IdentityFromContext(r.Context())
// id.Subject, id.Method, id.Scopes

OAuth / JWT with OIDC Auto-Discovery

When OAuthJWKSURL is omitted, the library fetches {issuer}/.well-known/openid-configuration to resolve the JWKS endpoint automatically. Works with Google, Auth0, Keycloak, Azure AD, Okta, Anthropic, and any other OIDC-compliant provider.

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

Explicit JWKS URL is still supported:

auth, err := authware.New(&authware.Config{
    Mode:          authware.ModeOAuth,
    OAuthIssuer:   "https://auth.example.com",
    OAuthAudience: "my-api",
    OAuthJWKSURL:  "https://auth.example.com/.well-known/jwks.json",
}, nil)

OAuth Proxy (MCP 2025-11-25)

The OAuthProxy bridges MCP clients that require public-client OAuth with upstream IdPs (e.g. Azure AD, Okta) that don't natively support RFC 7591 Dynamic Client Registration.

It provides four HTTP handlers per the MCP specification (2025-11-25):

  • ASMetadataHandler — serves /.well-known/oauth-authorization-server with RFC 8414 metadata (issuer derived from request origin; upstream discovery fetched once via sync.Once)
  • AuthorizeHandler — 302-redirects to the upstream IdP's authorization endpoint, forwarding all query parameters (PKCE, state, redirect_uri, etc.)
  • RegisterHandler — minimal RFC 7591 DCR shim returning a pre-configured client_id
  • TokenHandler — proxies token exchange requests to the upstream IdP

MCP spec default endpoint paths are /authorize, /token, /register:

proxy := authware.NewOAuthProxy(&authware.Config{
    OAuthAuthorizationServers: []string{"https://login.microsoftonline.com/tenant/v2.0"},
    OAuthClientID:             "my-app-client-id",
    OAuthClientSecret:         "my-app-client-secret", // optional: for confidential-client flows
}, 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())
}

All proxy JSON serialization uses go-jsonfast Builder with pooled buffers — zero encoding/json.Marshal calls on the hot path.

Nginx auth_request

The AuthCheckHandler returns an http.Handler compatible with nginx's auth_request module. On success it returns 200 with identity headers; on failure 401/403.

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

nginx configuration:

location /api/ {
    auth_request /check;
    auth_request_set $auth_subject $upstream_http_x_auth_subject;
    auth_request_set $auth_method  $upstream_http_x_auth_method;
    proxy_set_header X-Auth-Subject $auth_subject;
    proxy_set_header X-Auth-Method  $auth_method;
    proxy_pass http://backend;
}

location = /check {
    internal;
    proxy_pass http://127.0.0.1:9090/check;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

The AuthCheckHandler can be used to build a standalone auth-check microservice that sits behind nginx and validates every incoming request.

Environment Configuration

ConfigFromEnv reads AUTH_* environment variables for easy deployment. See .env.example for all available variables.

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

Auth Modes

Mode Config Fields Description
none Pass-through, no authentication
bearer BearerToken Static bearer token in Authorization: Bearer <token>
apikey APIKey, APIKeyHeader Static key in custom header or Authorization: ApiKey <key>
oauth OAuthIssuer + optional fields Full JWT validation with JWKS / OIDC discovery

Benchmarks

Measured on Intel Xeon Gold 6426Y (32 cores):

Benchmark ns/op B/op allocs/op
Bearer (accept) 20 0 0
Bearer (reject) 11 0 0
API Key 23 0 0
API Key (Authorization header) 36 0 0
None 4 0 0
OAuth HMAC HS256 1050 24 1
Scope check 32 0 0
Secure equal 22 0 0
Proxy: Register (DCR) 3896 6435 22
Proxy: AS Metadata (cached) 3119 6663 23
Proxy: Token 87596 50150 118
Middleware (Bearer) 329 624 7

Hot paths (Bearer, API Key, None, Scope check, Secure equal) are zero-allocation. OAuth HMAC has a single allocation for copyClaims string detachment. Proxy handlers use pooled go-jsonfast builders and a shared http.Client for connection pooling. All JSON parsing (OIDC discovery, AS metadata) uses go-jsonfast FindField instead of encoding/json, eliminating reflection overhead.

📁 Project Structure

go-authware/
├── authware.go         # Constructor, config, middleware, context helpers
├── types.go            # Config, Identity, Authenticator interface
├── bearer.go           # Bearer token authenticator
├── apikey.go           # API key authenticator
├── none.go             # Pass-through authenticator
├── jwt.go              # OAuth/JWT validation with JWKS auto-discovery
├── oidc.go             # OIDC auto-discovery
├── proxy.go            # OAuth proxy (AS metadata, DCR shim, token proxy)
├── challenge.go        # HTTP challenge/error response formatting
├── handler.go          # Nginx auth_request handler
├── env.go              # Environment variable configuration
├── doc.go              # Package documentation
├── bench_test.go       # Performance benchmarks (12 benchmarks)
├── default.pgo         # PGO profile for build optimization
├── .golangci.yml       # Ultra-strict linter config (27 linters)
├── Makefile            # Build, test, bench, lint
├── CONTRIBUTING.md     # Contribution guidelines
├── SECURITY.md         # Security policy
└── LICENSE             # MIT

🤝 Contributing

Contributions are welcome. Please fork the repository, create a feature branch, and submit a pull request.

For contribution guidelines, see CONTRIBUTING.md.


🔖 Versioning

We use SemVer for versioning. For available versions, see the tags on this repository.


👤 Authors

  • Paolo FabrisInitial workubyte.it

See also the list of contributors who participated in this project.

📄 License

This project is licensed under the MIT License — see the LICENSE file for details.


☕ Support This Project

If go-authware has been useful for your projects, consider supporting its development:

Buy Me A Coffee


Star this repository if you find it useful.

For questions, issues, or contributions, visit our GitHub repository.

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

Examples

Constants

View Source
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

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

RequireScopes returns an HTTP middleware that verifies the authenticated identity (from context) possesses all the specified scopes. Must be applied after Middleware.

func WithIdentity

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

WithIdentity returns a copy of ctx carrying the given Identity.

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

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

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.

Jump to

Keyboard shortcuts

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