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 ¶
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 = 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 ¶
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 ¶
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 ¶
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 ¶
Challenges returns an owned copy of complete WWW-Authenticate values.
func (*Error) GoString ¶
GoString returns the same sanitized diagnostic used by Error.Error.
func (*Error) StatusCode ¶
StatusCode returns the recommended HTTP status, or 500 for a zero Error.
func (*Error) Unwrap ¶
Unwrap exposes only a package category or identity.ErrConflict.
type VerifyFunc ¶
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.