authn

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package authn selects one configured Bearer or EdgeIAM verifier from an HTTP Authorization header and transports the verified caller to protected handlers. NewCognitoVerifier provides local RS256 Cognito access-token verification with direct JSON v2 and bounded JWKS caching. It does not check token revocation, authorize actions, or infer trust from a gateway, a TLS field, an action header or forwarded headers.

Use New to configure explicit verifier functions. Authenticate supports custom HTTP pipelines without writing responses or installing context. Handler adds required-authentication middleware. Public routes omit that middleware; CORS and preflight policy belong to the gateway or outer middleware.

Verifiers must authenticate credentials before returning a caller and classify rejection with ErrInvalidCredentials. Adapt iamproof.ErrInvalidProof explicitly when wiring an IAM proof verifier; this package does not import AWS packages. Never retry another verifier after rejection. Authenticate and authorize the selected action before committing a response or starting a stream.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidConfiguration means a constructor rejected its configuration, or a method was called on a nil or zero value or with a nil context.
	// It is returned directly, not wrapped in an [*Error].
	ErrInvalidConfiguration = errors.New("authn: invalid configuration")

	// ErrMissingCredentials means the request has no Authorization field (401 with all configured challenges).
	ErrMissingCredentials = errors.New("authn: missing credentials")

	// ErrMalformedCredentials means the Authorization field is repeated, comma-combined, empty or not valid scheme framing (400).
	ErrMalformedCredentials = errors.New("authn: malformed credentials")

	// ErrUnsupportedScheme means the credential uses a well-formed scheme that is not configured (401 with all configured challenges).
	ErrUnsupportedScheme = errors.New("authn: unsupported scheme")

	// ErrHeaderTooLarge means the Authorization field exceeds [Config.MaxAuthorizationBytes] (431, without a challenge).
	ErrHeaderTooLarge = errors.New("authn: authorization header too large")

	// ErrInvalidCredentials means the selected verifier definitely rejected the credential (401 with that scheme's challenge).
	// A [VerifyFunc] returns it to classify a rejection.
	ErrInvalidCredentials = errors.New("authn: invalid credentials")

	// ErrUnavailable means verification could not complete, for example because a dependency failed or a verifier returned an unclassified error (503, without a challenge).
	ErrUnavailable = errors.New("authn: verification unavailable")

	// ErrVerifierContract means a verifier reported success with an unacceptable caller, such as the wrong kind or source (500).
	// It indicates a composition fault, not a client error.
	ErrVerifierContract = errors.New("authn: invalid verifier result")
)

Inspectable failure categories never contain credentials or provider errors. Match them with errors.Is. Request-time failures are wrapped in an *Error whose Error.StatusCode is noted below.

Functions

This section is empty.

Types

type Authenticator

type Authenticator struct {
	// contains filtered or unexported fields
}

Authenticator is an immutable selector configured by New. Its zero value is unconfigured. Shared use requires concurrency-safe verifier functions.

func New

func New(cfg Config) (*Authenticator, error)

New validates and copies cfg without I/O or invoking a verifier. It returns a nil Authenticator and ErrInvalidConfiguration for invalid configuration.

func (*Authenticator) Authenticate

func (a *Authenticator) Authenticate(ctx context.Context, headers http.Header) (identity.Caller, error)

Authenticate selects at most one verifier and returns a verified caller. It neither modifies headers nor installs context. Callers must not mutate headers concurrently. Every failure returns an anonymous zero caller.

An established caller fails with identity.ErrConflict before credential work. Runtime failures are *Error with sanitized categories; caller cancellation returns ctx.Err directly. Nil contexts and nil/zero Authenticators return ErrInvalidConfiguration. No provider diagnostic or context cause is exposed.

Example
package main

import (
	"context"
	"encoding/json/v2"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/asteroid-computing/go-lambda-edge/authn"
	"github.com/asteroid-computing/go-lambda-edge/identity"
)

func main() {
	// This example demonstrates a custom JSON error envelope.
	// Its verifier rejects all credentials and is never called for the missing-header case.
	a, err := authn.New(authn.Config{Bearer: func(context.Context, string) (identity.Caller, error) {
		return identity.Caller{}, authn.ErrInvalidCredentials
	}})
	if err != nil {
		fmt.Println(err)
		return
	}
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		caller, err := a.Authenticate(r.Context(), r.Header)
		if err != nil {
			status := http.StatusInternalServerError
			if failure, ok := errors.AsType[*authn.Error](err); ok {
				status = failure.StatusCode()
				for _, challenge := range failure.Challenges() {
					w.Header().Add("WWW-Authenticate", challenge)
				}
			} else if r.Context().Err() != nil {
				status = http.StatusServiceUnavailable
			}
			body, encodeErr := json.Marshal(struct {
				Error string `json:"error"`
			}{Error: http.StatusText(status)})
			if encodeErr != nil {
				http.Error(w, "internal error", http.StatusInternalServerError)
				return
			}
			w.Header().Set("Cache-Control", "no-store")
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(status)
			if _, err := w.Write(body); err != nil {
				return // The transport can no longer deliver this response.
			}
			return
		}
		// A real custom pipeline would install caller, authorize its selected action, then invoke the application.
		// No such success is possible here.
		_ = caller
		http.Error(w, "no action authorized", http.StatusForbidden)
	})
	w := httptest.NewRecorder()
	h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/", nil))
	fmt.Println(w.Code)
	fmt.Println(w.Header().Get("WWW-Authenticate"))
	fmt.Println(w.Body.String())
}
Output:
401
Bearer realm="edge"
{"error":"Unauthorized"}

func (*Authenticator) Handler

func (a *Authenticator) Handler(next http.Handler) (http.Handler, error)

Handler constructs required-authentication middleware. It validates next, including typed nils. Success installs one caller in a derived request context and invokes next exactly once. It leaves the original request and headers intact.

Failure writes a generic text response, no-store and the applicable challenges; next is not invoked. Caller cancellation attempts 503 without a challenge, but delivery is not guaranteed. Provider/handler panics propagate. Public routes and CORS/preflight must be configured separately; there is no OPTIONS bypass.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/asteroid-computing/go-lambda-edge/actionheader"
	"github.com/asteroid-computing/go-lambda-edge/authn"
	"github.com/asteroid-computing/go-lambda-edge/iamproof"
	"github.com/asteroid-computing/go-lambda-edge/identity"
)

func main() {
	proofVerifier, err := iamproof.NewVerifier("eu-west-2", "orders.production")
	if err != nil {
		fmt.Println(err)
		return
	}
	verifyIAM := func(ctx context.Context, token string) (identity.Caller, error) {
		caller, err := proofVerifier.Verify(ctx, token)
		if errors.Is(err, iamproof.ErrInvalidProof) {
			return identity.Caller{}, authn.ErrInvalidCredentials
		}
		return caller, err
	}
	// Synthetic fixture only.
	// Replace this function with your actual issuer/key/signature/expiry/client-policy verifier.
	// Parsing claims is NOT verification.
	verifyBearer := func(_ context.Context, token string) (identity.Caller, error) {
		if token != "synthetic-example-token" {
			return identity.Caller{}, authn.ErrInvalidCredentials
		}
		claims, err := identity.NewClaims(map[string]any{"iss": "https://issuer.example", "sub": "alice"})
		if err != nil {
			return identity.Caller{}, err
		}
		return identity.NewJWT(claims, identity.SourceLocallyVerifiedToken)
	}
	a, err := authn.New(authn.Config{Bearer: verifyBearer, IAMProof: verifyIAM, Realm: "orders"})
	if err != nil {
		fmt.Println(err)
		return
	}
	selector, err := actionheader.NewSelector("Action")
	if err != nil {
		fmt.Println(err)
		return
	}
	dispatcher := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		action, err := selector.Parse(r.Header)
		if err != nil {
			http.Error(w, "invalid action", http.StatusBadRequest)
			return
		}
		// Consumer-owned authorization: deny by default.
		// Neither successful token verification nor AWS account membership grants this action alone.
		caller := identity.FromContext(r.Context())
		allowed := false
		if jwt, ok := caller.JWT(); ok {
			sub, present := jwt.Subject()
			allowed = present && jwt.Issuer() == "https://issuer.example" && sub == "alice"
		}
		if iam, ok := caller.IAM(); ok {
			allowed = iam.PrincipalARN() == "arn:aws:iam::123456789012:user/Alice"
		}
		if !allowed || action != "orders.read" {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		// Execute the exact selection just authorized.
		// Extra consumer headers remain available through r.Header;
		// authn does not interpret them.
		w.WriteHeader(http.StatusNoContent)
	})
	protected, err := a.Handler(dispatcher)
	if err != nil {
		fmt.Println(err)
		return
	}
	// protected also goes directly to edge.New for Lambda registration.
	// This local example exercises only the synthetic Bearer path;
	// no AWS calls.
	r := httptest.NewRequest(http.MethodPost, "/", nil)
	r.Header.Set("Authorization", "Bearer synthetic-example-token")
	r.Header.Set("Action", "orders.read")
	w := httptest.NewRecorder()
	protected.ServeHTTP(w, r)
	fmt.Println(w.Code)
	// The malformed proof is rejected before any STS call, using the mapping.
	r.Header.Set("Authorization", "EdgeIAM v2.invalid")
	w = httptest.NewRecorder()
	protected.ServeHTTP(w, r)
	fmt.Println(w.Code)
	fmt.Println(w.Header().Get("WWW-Authenticate"))
}
Output:
204
401
EdgeIAM realm="orders"

type CognitoClient

type CognitoClient struct {
	ClientID     string
	Audience     string
	AllowUnbound bool
}

CognitoClient restricts one app client independently of its resource audience. Audience requires an exact string aud. AllowUnbound additionally permits an absent aud; with an empty Audience it permits only absent aud. At least one of Audience or AllowUnbound is required. No policy accepts an arbitrary aud.

type CognitoConfig

type CognitoConfig struct {
	// Issuer is an exact HTTPS pool URL, without trailing slash, escaped path, dot segments, credentials, query or fragment.
	// JWKS is fetched from this trusted endpoint plus /.well-known/jwks.json, never from token headers.
	Issuer string
	// Clients must contain at least one policy with a distinct nonempty ClientID.
	Clients []CognitoClient
	// ClockSkew permits 0..5 minutes of leeway.
	// Zero does not extend token life.
	ClockSkew time.Duration
	// CacheTTL defaults to 15 minutes;
	// explicit values allow 1 minute..24 hours.
	// Expired keys are not used during outages.
	// No token results are cached.
	CacheTTL time.Duration
	// Timeout defaults to 5 seconds;
	// explicit values must be positive and <=1 minute.
	Timeout time.Duration
	// Transport defaults to http.DefaultTransport.
	// A supplied transport remains caller-owned, must support concurrency and must honor context cancellation.
	Transport http.RoundTripper
	// MaxTokenBytes defaults to 16 KiB;
	// explicit values allow 1 byte..1 MiB.
	// The separate Authorization field bound also includes the scheme/framing.
	MaxTokenBytes int
	// ClaimsBudget defaults to identity's 256 KiB weighted allowance.
	// Explicit values allow 1 byte..6 MiB, independently of the compact token byte limit.
	ClaimsBudget int
}

CognitoConfig supplies explicit trust and resource policy for access tokens.

type CognitoVerifier

type CognitoVerifier struct {
	// contains filtered or unexported fields
}

CognitoVerifier verifies a narrow RS256 Cognito access-token profile using JSON v2 and standard-library cryptography. It does not check revocation or grant permissions. It is safe for concurrent use, but must not be copied. Its zero value is unconfigured. Reuse one instance to share its bounded cache.

func NewCognitoVerifier

func NewCognitoVerifier(cfg CognitoConfig) (*CognitoVerifier, error)

NewCognitoVerifier validates and copies cfg without I/O. Invalid configuration returns a nil verifier and ErrInvalidConfiguration. It trusts the configured issuer endpoint; it does not establish that the operator supplied an AWS URL.

Example
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/asteroid-computing/go-lambda-edge/authn"
)

func main() {
	verifier, err := authn.NewCognitoVerifier(authn.CognitoConfig{
		Issuer: "https://issuer-cognito-idp.eu-west-2.amazonaws.com/eu-west-2_example",
		Clients: []authn.CognitoClient{
			{ClientID: "user-app-client", Audience: "https://orders.example"},
			// Cognito M2M grants do not provide resource binding.
			// This permits absent aud only;
			// any audience on this client's token is rejected.
			{ClientID: "machine-app-client", AllowUnbound: true},
		},
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	a, err := authn.New(authn.Config{Bearer: verifier.Verify})
	if err != nil {
		fmt.Println(err)
		return
	}
	// Constructors perform no I/O.
	// Verification is lazy;
	// optional Warm(ctx) can fetch keys during application initialization.
	// Reuse the verifier.
	// Missing credentials are rejected without contacting the issuer.
	_, err = a.Authenticate(context.Background(), make(http.Header))
	fmt.Println(err)
}
Output:
authn: missing credentials

func (*CognitoVerifier) Verify

func (v *CognitoVerifier) Verify(ctx context.Context, token string) (identity.Caller, error)

Verify returns one locally verified caller without installing it in context. It accepts access tokens only, with integer exp/iat and optional nbf, and a string aud when present. Rejections match ErrInvalidCredentials; unavailable JWKS matches ErrUnavailable. Caller cancellation returns ctx.Err, never its cause. Every failure returns a zero caller and sanitized diagnostics.

func (*CognitoVerifier) Warm

func (v *CognitoVerifier) Warm(ctx context.Context) error

Warm obtains a fresh usable JWKS snapshot, sharing any required fetch. It obeys refresh/backoff gates; it does not force refresh, validate a token or start a background refresh loop. Failures are ErrUnavailable, caller ctx.Err or ErrInvalidConfiguration for a nil context or unconfigured verifier.

type Config

type Config struct {
	// Bearer and IAMProof are independently optional;
	// at least one is required.
	// Each function must be safe for concurrent use when sharing an Authenticator.
	Bearer   VerifyFunc
	IAMProof VerifyFunc
	// Realm is a challenge label, not a proof audience or issuer restriction.
	// Empty defaults to "edge";
	// explicit values allow 1-256 printable ASCII bytes.
	Realm string
	// MaxAuthorizationBytes bounds represented Authorization value bytes before trimming.
	// Zero defaults to 16 KiB;
	// explicit values must be in 1..1 MiB.
	// This neither changes iamproof's 8 KiB proof limit nor bounds all headers.
	MaxAuthorizationBytes int
}

Config supplies explicit verifier dependencies and HTTP framing policy.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error describes a sanitized HTTP authentication failure. Inspect its category with errors.Is and its HTTP treatment with errors.AsType[*Error]. Its private state never retains a supplied credential or an underlying provider error. The zero value describes an internal failure, without a challenge.

func (*Error) Challenges

func (e *Error) Challenges() []string

Challenges returns an owned copy of complete WWW-Authenticate values.

func (*Error) Error

func (e *Error) Error() string

Error returns a diagnostic containing only the sanitized category.

func (*Error) GoString

func (e *Error) GoString() string

GoString returns the same sanitized diagnostic used by Error.Error.

func (*Error) StatusCode

func (e *Error) StatusCode() int

StatusCode returns the recommended HTTP status, or 500 for a zero Error.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes only a package category or identity.ErrConflict.

type VerifyFunc

type VerifyFunc func(ctx context.Context, credential string) (identity.Caller, error)

VerifyFunc authenticates a credential without its HTTP scheme. It must return ErrInvalidCredentials for a definite rejection; unknown errors are treated as unavailable. Success requires JWT/SourceLocallyVerifiedToken for Bearer or IAM/SourceVerifiedIAMProof for EdgeIAM. Constructors alone do not verify tokens.

Jump to

Keyboard shortcuts

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