Documentation
¶
Overview ¶
Package auth verifies JWT access tokens in Lambda functions: Powertools for AWS Lambda (Python)'s Auth utility (auth_alpha). Upstream marks the utility alpha, so this API may change while it is.
Verifying a token ¶
A JWTVerifier trusts one issuer, one or more audiences (the resource the function serves) and an explicit list of asymmetric algorithms. Build it once, outside the handler, so that warm invocations reuse its signing keys:
verifier, err := auth.NewJWTVerifier(
"https://idp.example.com/",
[]string{"https://orders.example.com"},
[]auth.Algorithm{auth.RS256},
auth.WithRequiredClaims("sub"),
auth.WithExpectedClaims(map[string]string{"token_use": "access"}),
)
...
claims, err := verifier.Verify(ctx, token)
Verify checks, in Powertools' order: the header (an accepted alg, a kid, no crit, no unencoded payload), the one published key that matches it (kid, key type, and the alg, use and key_ops the provider published), the signature, the required claims (iss, aud and exp always), the issuer (byte for byte), the audience, exp, nbf and iat with the clock skew, then the expected claims and headers. The returned Claims has accessors for the registered claims, Get and GetString for the others, and As to decode them into a struct of yours.
VerifyAuthorizationHeader takes a whole "Bearer <token>" header value and VerifyRequest an *http.Request. NewCognitoVerifier is the profile of an Amazon Cognito user pool, which accepts resource-bound access tokens of one app client only. AnyOf routes a token to the verifier of its issuer, for functions that trust several.
Signing keys ¶
The keys come from the issuer's OpenID configuration (/.well-known/openid-configuration, which must name the issuer exactly), from WithJWKSURI, or from a static WithJWKS set that needs no network. Fetched keys are cached as Powertools caches them:
- they are fetched on first use, never in the background: Lambda freezes the execution environment between invocations. Prefetch fetches them during initialization instead;
- they are trusted for WithJWKSMaxAge (five minutes), then refreshed. Keys whose refresh fails are never served past that age: the verification fails with ErrJWKSFetch;
- a token whose kid is unknown refreshes the set, at most once per WithUnknownKIDCooldown (five minutes), so that key rotation works and forged tokens cannot hammer the provider;
- a refresh replaces the whole set, so removed keys disappear;
- failed refreshes back off for 1, 2, 4, 8, 16, then 30 seconds;
- concurrent verifications share one refresh, each waiting at most WithTimeout (three seconds), which also bounds the requests;
- verifiers with the same issuer, key URL, timings and HTTP client share their keys while one of them is alive.
Requests use HTTPS with certificate verification, follow no redirect, and read at most 1 MiB.
Event Handler middleware ¶
Require is Powertools' require(): a net/http middleware that refuses a request without a valid token granting the required scopes, and hands the verified claims to the handler through the request's context:
protect, err := auth.Require(verifier, auth.WithScopes("orders:read"))
...
mux.Handle("GET /orders", protect(listOrders))
func listOrders(w http.ResponseWriter, r *http.Request) {
claims, _ := auth.ClaimsFromContext(r.Context())
...
}
A missing or invalid token gets 401, a missing scope or a refused WithAuthorize policy 403, unavailable keys 503, with RFC 6750's WWW-Authenticate challenge and a JSON {"message": ...} body. WithErrorResponse replaces that response.
Lambda authorizers ¶
AuthorizeToken and AuthorizeRequest answer REST API TOKEN and REQUEST authorizers, AuthorizeHTTPV1 and AuthorizeHTTP the HTTP API ones with an IAM policy, AuthorizeHTTPSimple with a simple response. Invalid tokens and missing scopes are denied; unavailable keys fail the invocation so that API Gateway answers 5xx. An IAM Allow names the token's sub as its principal and grants the invoked method or route ARN only, never a wildcard. WithContextClaims copies selected scalar claims into the authorizer context. Disable API Gateway's authorizer caching, or key it carefully: a cached Allow outlives the token.
Errors ¶
Every failure is an *Error with a stable Reason and a Retryable flag; its message is fixed and never carries the token, its claims or the provider's answer. The sentinels follow Powertools' hierarchy: ErrInvalidToken matches ErrMissingToken, ErrInvalidClaims, ErrTokenExpired and ErrInvalidSignature; ErrForbidden matches ErrInsufficientScope; ErrJWKSFetch is the retryable one. Configuration errors match ErrInvalidConfig, authorizer events of the wrong shape ErrInvalidEvent. The package logs nothing, except to the POWERTOOLS_DEBUG log.
Testing ¶
The authtest package has a Verifier that returns fixed claims without keys or network, the Go side of Powertools' mock_claims.
Differences from Powertools Python ¶
- Python's ValueError at construction is an error matching ErrInvalidConfig. Require returns one too, for invalid scopes.
- Require is net/http middleware: the claims live in the request's context rather than in app.context, WithAuthorize receives the request with the claims, and WithErrorResponse writes the response itself.
- The authorizer functions take the aws-lambda-go event types, one per event kind, instead of a dict and a response_format; the IAM statement lists its Action, as aws-lambda-go encodes it.
- VerifyAuthorizationHeader treats an empty value as a missing header, since a Go string cannot be None.
- JSON with duplicate member names, in a token, a key set or a discovery document, is refused; Python keeps the last value.
- A key whose curve does not fit an ES algorithm is an invalid token, where PyJWT reports invalid claims. A private JWK verifies with its public half.
- EdDSA covers Ed25519 keys; Ed448 keys, which neither the Go standard library nor jwx supports natively, are unusable.
- A refresh cut short because its caller's context ended, rather than by the timeout, is not counted as a provider failure: the next caller refreshes at once instead of backing off. Python has no such case.
- A numeric authorizer context claim beyond int64 is copied as a float64.
Index ¶
- Constants
- Variables
- func AuthorizeHTTP(ctx context.Context, v Verifier, ...) (events.APIGatewayV2CustomAuthorizerIAMPolicyResponse, error)
- func AuthorizeHTTPSimple(ctx context.Context, v Verifier, ...) (events.APIGatewayV2CustomAuthorizerSimpleResponse, error)
- func AuthorizeHTTPV1(ctx context.Context, v Verifier, ...) (events.APIGatewayCustomAuthorizerResponse, error)
- func AuthorizeRequest(ctx context.Context, v Verifier, ...) (events.APIGatewayCustomAuthorizerResponse, error)
- func AuthorizeToken(ctx context.Context, v Verifier, ...) (events.APIGatewayCustomAuthorizerResponse, error)
- func ContextWithClaims(ctx context.Context, claims *Claims) context.Context
- func Require(v Verifier, opts ...RequireOption) (func(http.Handler) http.Handler, error)
- type Algorithm
- type AuthorizerOption
- type Claims
- func (c *Claims) As[T any]() (T, error)
- func (c *Claims) Audience() []string
- func (c *Claims) ExpiresAt() time.Time
- func (c *Claims) Get(name string) (any, bool)
- func (c *Claims) GetString(name string) (string, bool)
- func (c *Claims) Has(name string) bool
- func (c *Claims) ID() string
- func (c *Claims) IssuedAt() time.Time
- func (c *Claims) Issuer() string
- func (c *Claims) Lookup(name string) (jsontext.Value, bool)
- func (c *Claims) MarshalJSON() ([]byte, error)
- func (c *Claims) NotBefore() time.Time
- func (c *Claims) Raw() jsontext.Value
- func (c *Claims) Scopes() []string
- func (c *Claims) Subject() string
- type Error
- type Failure
- type JWTVerifier
- type Option
- func WithClockSkew(d time.Duration) Option
- func WithExpectedClaims(claims map[string]string) Option
- func WithExpectedHeaders(headers map[string]string) Option
- func WithHTTPClient(client *http.Client) Option
- func WithJWKS(jwks []byte) Option
- func WithJWKSMaxAge(d time.Duration) Option
- func WithJWKSURI(uri string) Option
- func WithRequiredClaims(names ...string) Option
- func WithTimeout(d time.Duration) Option
- func WithUnknownKIDCooldown(d time.Duration) Option
- type Reason
- type RequireOption
- type ScopeOption
- type Verifier
Examples ¶
Constants ¶
const ( DefaultClockSkew = 60 * time.Second DefaultTimeout = 3 * time.Second DefaultJWKSMaxAge = 300 * time.Second DefaultUnknownKIDCooldown = 300 * time.Second )
The defaults of Powertools Python's JWTVerifier.
Variables ¶
var ( // ErrInvalidToken matches every failure that means the bearer token // cannot be trusted: missing, malformed, badly signed, expired, or with // claims that do not fit the profile. ErrInvalidToken = errors.New("invalid access token") // ErrMissingToken matches a request without an Authorization header. ErrMissingToken = errors.New("missing access token") // ErrInvalidClaims matches a missing or mismatching claim. ErrInvalidClaims = errors.New("invalid access token claims") // ErrTokenExpired matches a token past its exp claim and the clock // skew. ErrTokenExpired = errors.New("access token expired") // ErrInvalidSignature matches a signature the signing key rejects. ErrInvalidSignature = errors.New("invalid access token signature") // ErrForbidden matches a verified caller who lacks a permission. ErrForbidden = errors.New("forbidden") // ErrInsufficientScope matches a verified caller without a required // scope. ErrInsufficientScope = errors.New("insufficient scope") // ErrJWKSFetch matches signing keys that could not be fetched: the // identity provider is unreachable, slow, or answered something // unusable. It is the one retryable failure. ErrJWKSFetch = errors.New("unable to retrieve verification keys") // ErrInvalidConfig matches a configuration the package refuses: an // issuer that is not an HTTPS URL, a symmetric algorithm, a scope that // is not a scope token. The message names the problem, never the value. ErrInvalidConfig = errors.New("invalid auth configuration") // ErrInvalidEvent matches an authorizer event of the wrong type or // version, or whose method or route ARN is not concrete. ErrInvalidEvent = errors.New("invalid authorizer event") )
The sentinels an *Error matches with errors.Is. They form Powertools' hierarchy: every reason about the token itself matches ErrInvalidToken, and an insufficient scope matches ErrForbidden.
Functions ¶
func AuthorizeHTTP ¶
func AuthorizeHTTP(ctx context.Context, v Verifier, event events.APIGatewayV2CustomAuthorizerV2Request, opts ...AuthorizerOption) (events.APIGatewayV2CustomAuthorizerIAMPolicyResponse, error)
AuthorizeHTTP answers an HTTP API authorizer with payload format 2.0 with an IAM policy for the invoked route. It works like AuthorizeRequest.
func AuthorizeHTTPSimple ¶
func AuthorizeHTTPSimple(ctx context.Context, v Verifier, event events.APIGatewayV2CustomAuthorizerV2Request, opts ...AuthorizerOption) (events.APIGatewayV2CustomAuthorizerSimpleResponse, error)
AuthorizeHTTPSimple answers an HTTP API authorizer with payload format 2.0 with a simple response, which the authorizer must have enabled: isAuthorized true when the token is valid and grants the required scopes. It requires no sub claim.
func AuthorizeHTTPV1 ¶
func AuthorizeHTTPV1(ctx context.Context, v Verifier, event events.APIGatewayV2CustomAuthorizerV1Request, opts ...AuthorizerOption) (events.APIGatewayCustomAuthorizerResponse, error)
AuthorizeHTTPV1 answers an HTTP API authorizer with payload format 1.0, which takes an IAM policy. It works like AuthorizeRequest.
func AuthorizeRequest ¶
func AuthorizeRequest(ctx context.Context, v Verifier, event events.APIGatewayCustomAuthorizerRequestTypeRequest, opts ...AuthorizerOption) (events.APIGatewayCustomAuthorizerResponse, error)
AuthorizeRequest answers a REST API REQUEST authorizer with an IAM policy, the token coming from the Authorization header. It works like AuthorizeToken.
func AuthorizeToken ¶
func AuthorizeToken(ctx context.Context, v Verifier, event events.APIGatewayCustomAuthorizerRequest, opts ...AuthorizerOption) (events.APIGatewayCustomAuthorizerResponse, error)
AuthorizeToken answers a REST API TOKEN authorizer: an IAM policy that allows the invoked method when the token in authorizationToken is valid and grants the required scopes, and denies it otherwise. The principal is the token's sub claim, which an Allow requires.
Unavailable signing keys return an error matching ErrJWKSFetch, so that API Gateway answers 5xx instead of caching a Deny. An event of another type, or whose method ARN is not concrete, matches ErrInvalidEvent.
Disable API Gateway's authorizer caching, or key it carefully: a cached Allow outlives the token's expiry until its TTL ends.
Example ¶
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json/v2"
"fmt"
"time"
"github.com/aws/aws-lambda-go/events"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const (
issuer = "https://idp.example.com/"
resource = "https://orders.example.com"
)
// identityProvider plays the identity provider of the examples: it returns
// the key set it publishes and an access token it issued.
func identityProvider() (jwks []byte, token string) {
private := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
public, err := jwk.Import[jwk.Key](private.Public())
must(err)
must(public.Set(jwk.KeyIDKey, "key-1"))
set := jwk.NewSet()
must(set.AddKey(public))
jwks, err = json.Marshal(set)
must(err)
claims, err := json.Marshal(map[string]any{
"iss": issuer,
"aud": resource,
"sub": "user-1",
"scope": "orders:read",
"token_use": "access",
"tenant": "acme",
"exp": time.Now().Add(time.Hour).Unix(),
})
must(err)
headers := jws.NewHeaders()
must(headers.Set(jws.KeyIDKey, "key-1"))
signed, err := jws.Sign(claims, jws.WithKey(jwa.EdDSA(), private, jws.WithProtectedHeaders(headers)))
must(err)
return jwks, string(signed)
}
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
jwks, token := identityProvider()
verifier, err := auth.NewJWTVerifier(issuer, []string{resource}, []auth.Algorithm{auth.EdDSA}, auth.WithJWKS(jwks))
if err != nil {
panic(err)
}
// The handler of a REST API TOKEN authorizer, for lambda.Start.
authorizer := func(ctx context.Context, event events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) {
return auth.AuthorizeToken(ctx, verifier, event, auth.WithScopes("orders:read"), auth.WithContextClaims("tenant"))
}
out, err := authorizer(context.Background(), events.APIGatewayCustomAuthorizerRequest{
Type: "TOKEN",
AuthorizationToken: "Bearer " + token,
MethodArn: "arn:aws:execute-api:eu-west-1:123456789012:abc123/prod/GET/orders",
})
if err != nil {
panic(err)
}
fmt.Println(out.PrincipalID, out.PolicyDocument.Statement[0].Effect, out.Context)
}
Output: user-1 Allow map[tenant:acme]
func ContextWithClaims ¶
ContextWithClaims returns a copy of ctx that carries claims. Require stores the verified claims this way for the handlers it protects; tests can do the same to call a handler directly.
func Require ¶
Require returns a net/http middleware that lets a request through only with a valid bearer token granting every required scope: Powertools' Event Handler require(). The verified claims travel in the request's context, where ClaimsFromContext finds them; they never outlive the request.
A missing or invalid token is answered with 401, a missing scope or a refused authorization with 403, unavailable signing keys with 503, all with the WWW-Authenticate header of RFC 6750. An error from a Verifier that is not an *Error is answered with 500.
An invalid scope fails with ErrInvalidConfig.
Example ¶
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json/v2"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const (
issuer = "https://idp.example.com/"
resource = "https://orders.example.com"
)
// identityProvider plays the identity provider of the examples: it returns
// the key set it publishes and an access token it issued.
func identityProvider() (jwks []byte, token string) {
private := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
public, err := jwk.Import[jwk.Key](private.Public())
must(err)
must(public.Set(jwk.KeyIDKey, "key-1"))
set := jwk.NewSet()
must(set.AddKey(public))
jwks, err = json.Marshal(set)
must(err)
claims, err := json.Marshal(map[string]any{
"iss": issuer,
"aud": resource,
"sub": "user-1",
"scope": "orders:read",
"token_use": "access",
"tenant": "acme",
"exp": time.Now().Add(time.Hour).Unix(),
})
must(err)
headers := jws.NewHeaders()
must(headers.Set(jws.KeyIDKey, "key-1"))
signed, err := jws.Sign(claims, jws.WithKey(jwa.EdDSA(), private, jws.WithProtectedHeaders(headers)))
must(err)
return jwks, string(signed)
}
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
jwks, token := identityProvider()
verifier, err := auth.NewJWTVerifier(issuer, []string{resource}, []auth.Algorithm{auth.EdDSA}, auth.WithJWKS(jwks))
if err != nil {
panic(err)
}
protect, err := auth.Require(verifier, auth.WithScopes("orders:read"))
if err != nil {
panic(err)
}
orders := protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, _ := auth.ClaimsFromContext(r.Context())
if _, err := fmt.Fprintln(w, "orders of", claims.Subject()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}))
for _, authorization := range []string{"Bearer " + token, ""} {
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/orders", http.NoBody)
if authorization != "" {
r.Header.Set("Authorization", authorization)
}
w := httptest.NewRecorder()
orders.ServeHTTP(w, r)
fmt.Print(w.Code, " ", w.Body.String())
}
}
Output: 200 orders of user-1 401 {"message":"Unauthorized"}
Types ¶
type Algorithm ¶
type Algorithm string
Algorithm is a JWS signature algorithm a verifier accepts.
const ( RS256 Algorithm = "RS256" RS384 Algorithm = "RS384" RS512 Algorithm = "RS512" PS256 Algorithm = "PS256" PS384 Algorithm = "PS384" PS512 Algorithm = "PS512" ES256 Algorithm = "ES256" ES384 Algorithm = "ES384" ES512 Algorithm = "ES512" ES256K Algorithm = "ES256K" EdDSA Algorithm = "EdDSA" )
The asymmetric algorithms a verifier can accept. HMAC algorithms and "none" are refused: a verifier holds public keys only.
type AuthorizerOption ¶
type AuthorizerOption interface {
// contains filtered or unexported methods
}
AuthorizerOption configures the Authorize functions.
func WithContextClaims ¶
func WithContextClaims(names ...string) AuthorizerOption
WithContextClaims copies the named claims into the authorizer's context, where the integration reads them. Only claims that are strings, booleans or finite numbers are copied; others are skipped, as are missing ones. Nothing is copied by default, and "claims" is reserved by API Gateway.
func WithOnError ¶
func WithOnError(record func(ctx context.Context, err *Error)) AuthorizerOption
WithOnError calls record with every authentication failure, for logs and metrics: it cannot change the outcome, which is a Deny for invalid credentials and an error for unavailable keys. Nothing is logged unless it does so.
type Claims ¶
type Claims struct {
// contains filtered or unexported fields
}
Claims are the claims of an access token, as Verify returns them once the signature, the issuer, the audience and the times have been checked. Claims are immutable and safe for concurrent use.
The accessors read the registered claims; Get, GetString and Lookup read any other one, and As decodes them all into a struct of yours.
func ClaimsFromContext ¶
ClaimsFromContext returns the claims Require verified for the request whose context is ctx.
func ParseClaims ¶
ParseClaims decodes a JWT claims set, which must be a JSON object. It verifies nothing: it is for Verifier implementations and for tests, while Verify is how claims come to be trusted.
func VerifyAuthorizationHeader ¶
VerifyAuthorizationHeader verifies the token of an HTTP Authorization header value, "Bearer <token>" with the scheme in any case. An empty value is a missing header (ErrMissingToken); any other scheme or shape is an invalid token.
func VerifyRequest ¶
VerifyRequest verifies the bearer token of r's Authorization header, with r's context. A request without the header fails with ErrMissingToken; one with more than one Authorization value is invalid, since nobody can tell which one a proxy in front of the function has checked.
It is the building block of Require, for handlers that authorize in their own way.
func (*Claims) As ¶
As decodes the claims set into a T, typically a struct with a field for each claim the application reads.
Example ¶
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json/v2"
"fmt"
"time"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const (
issuer = "https://idp.example.com/"
resource = "https://orders.example.com"
)
// identityProvider plays the identity provider of the examples: it returns
// the key set it publishes and an access token it issued.
func identityProvider() (jwks []byte, token string) {
private := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
public, err := jwk.Import[jwk.Key](private.Public())
must(err)
must(public.Set(jwk.KeyIDKey, "key-1"))
set := jwk.NewSet()
must(set.AddKey(public))
jwks, err = json.Marshal(set)
must(err)
claims, err := json.Marshal(map[string]any{
"iss": issuer,
"aud": resource,
"sub": "user-1",
"scope": "orders:read",
"token_use": "access",
"tenant": "acme",
"exp": time.Now().Add(time.Hour).Unix(),
})
must(err)
headers := jws.NewHeaders()
must(headers.Set(jws.KeyIDKey, "key-1"))
signed, err := jws.Sign(claims, jws.WithKey(jwa.EdDSA(), private, jws.WithProtectedHeaders(headers)))
must(err)
return jwks, string(signed)
}
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
jwks, token := identityProvider()
verifier, err := auth.NewJWTVerifier(issuer, []string{resource}, []auth.Algorithm{auth.EdDSA}, auth.WithJWKS(jwks))
if err != nil {
panic(err)
}
claims, err := verifier.Verify(context.Background(), token)
if err != nil {
panic(err)
}
type caller struct {
Subject string `json:"sub"`
Tenant string `json:"tenant"`
}
who, err := claims.As[caller]()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", who)
}
Output: {Subject:user-1 Tenant:acme}
func (*Claims) Audience ¶
Audience returns the aud claim, which a token may carry as a string or as a list of strings.
func (*Claims) Get ¶
Get returns the claim decoded as a JSON value: nil, bool, float64, string, []any or map[string]any.
func (*Claims) MarshalJSON ¶
MarshalJSON returns the claims set as the token carried it, so that claims can be logged or returned as they are.
type Error ¶
type Error struct {
// Reason says why the request failed.
Reason Reason
// Retryable reports whether a later attempt may succeed, which is only
// the case when the signing keys were unavailable.
Retryable bool
}
Error is an authentication or authorization failure. Its message is fixed for its Reason and never carries the token, its claims or the identity provider's answer, so that it is safe to log; the cause of a failure is deliberately dropped.
type Failure ¶
type Failure struct {
// StatusCode is 401 for a missing or invalid token, 403 for a missing
// scope or a refused custom authorization, 503 when the signing keys
// are unavailable.
StatusCode int
// Header holds the WWW-Authenticate challenge RFC 6750 prescribes for
// the failure, if any. Keep it in a custom response.
Header http.Header
// Reason is the stable reason of the failure.
Reason Reason
// Retryable reports whether the same request may succeed later.
Retryable bool
}
Failure describes a request Require refuses: the status and headers of the default response, and why. It carries nothing from the token, so a custom response or a log line built from it leaks nothing.
type JWTVerifier ¶
type JWTVerifier struct {
// contains filtered or unexported fields
}
JWTVerifier verifies the JWT access tokens of one issuer. Build it once, outside the handler, so that warm invocations reuse its signing keys; it is safe for concurrent use.
func NewCognitoVerifier ¶
func NewCognitoVerifier(userPoolID, clientID string, audience []string, opts ...Option) (*JWTVerifier, error)
NewCognitoVerifier verifies the access tokens of an Amazon Cognito user pool, bound to audience through Cognito's resource binding. Besides what NewJWTVerifier checks, the token must be an access token (token_use is "access") issued to clientID. Cognito ID tokens, and access tokens issued without the resource audience, are refused.
The issuer, the RS256 algorithm and the JWKS URI follow from the pool and cannot be changed: WithJWKSURI is refused, WithJWKS is allowed.
Example ¶
package main
import (
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const resource = "https://orders.example.com"
func main() {
verifier, err := auth.NewCognitoVerifier("eu-west-1_AbC123", "orders-client", []string{resource})
if err != nil {
panic(err)
}
fmt.Println(verifier.Issuer())
}
Output: https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbC123
func NewJWTVerifier ¶
func NewJWTVerifier(issuer string, audience []string, algorithms []Algorithm, opts ...Option) (*JWTVerifier, error)
NewJWTVerifier verifies the access tokens issuer signs for audience with one of algorithms. The issuer is an exact HTTPS URL; the token's iss claim must equal it byte for byte, and so must the issuer its OpenID configuration announces. At least one audience must be in the token's aud claim.
Without WithJWKS or WithJWKSURI, the signing keys come from the issuer's OpenID configuration. Configuration errors match ErrInvalidConfig and never quote the values they refuse.
Example ¶
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json/v2"
"fmt"
"time"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const (
issuer = "https://idp.example.com/"
resource = "https://orders.example.com"
)
// identityProvider plays the identity provider of the examples: it returns
// the key set it publishes and an access token it issued.
func identityProvider() (jwks []byte, token string) {
private := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))
public, err := jwk.Import[jwk.Key](private.Public())
must(err)
must(public.Set(jwk.KeyIDKey, "key-1"))
set := jwk.NewSet()
must(set.AddKey(public))
jwks, err = json.Marshal(set)
must(err)
claims, err := json.Marshal(map[string]any{
"iss": issuer,
"aud": resource,
"sub": "user-1",
"scope": "orders:read",
"token_use": "access",
"tenant": "acme",
"exp": time.Now().Add(time.Hour).Unix(),
})
must(err)
headers := jws.NewHeaders()
must(headers.Set(jws.KeyIDKey, "key-1"))
signed, err := jws.Sign(claims, jws.WithKey(jwa.EdDSA(), private, jws.WithProtectedHeaders(headers)))
must(err)
return jwks, string(signed)
}
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
jwks, token := identityProvider()
// Build the verifier once, outside the handler. Without WithJWKS, it
// fetches the keys from the issuer's OpenID configuration.
verifier, err := auth.NewJWTVerifier(issuer, []string{resource}, []auth.Algorithm{auth.EdDSA},
auth.WithJWKS(jwks),
auth.WithRequiredClaims("sub"),
auth.WithExpectedClaims(map[string]string{"token_use": "access"}),
)
if err != nil {
panic(err)
}
claims, err := verifier.Verify(context.Background(), token)
if err != nil {
panic(err)
}
fmt.Println(claims.Subject(), claims.Scopes())
_, err = verifier.Verify(context.Background(), "not a token")
fmt.Println(err)
}
Output: user-1 [orders:read] invalid access token
func (*JWTVerifier) Issuer ¶
func (v *JWTVerifier) Issuer() string
Issuer returns the issuer the verifier trusts.
func (*JWTVerifier) Prefetch ¶
func (v *JWTVerifier) Prefetch(ctx context.Context) error
Prefetch fetches the signing keys unless they are fresh, so that the first invocation does not wait for them. Call it during initialization; an identity provider outage then fails the cold start. It does nothing with a static key set. A failure matches ErrJWKSFetch.
func (*JWTVerifier) String ¶
func (*JWTVerifier) String() string
String hides the configuration, as Powertools' repr does.
func (*JWTVerifier) Verify ¶
Verify returns the claims of token, a JWT without the Bearer prefix.
The header must name an accepted algorithm and a kid, without crit or an unencoded payload; exactly one published key must match it. Then the signature, the issuer, the audience, the required claims, exp, nbf and iat (with the clock skew), and the expected claims and headers are checked, in Powertools' order. Failures match ErrInvalidToken, or ErrJWKSFetch when the signing keys are unavailable.
type Option ¶
type Option func(*config)
Option configures a JWTVerifier.
func WithClockSkew ¶
WithClockSkew sets the tolerance applied to exp, nbf and iat. The default is one minute; it must not be negative.
func WithExpectedClaims ¶
WithExpectedClaims requires string claims with exactly these values, such as {"token_use": "access"}, so that a token of another kind with the same audience is refused. A missing claim fails the check.
func WithExpectedHeaders ¶
WithExpectedHeaders requires JOSE header parameters with exactly these values, such as {"typ": "at+jwt"}.
func WithHTTPClient ¶
WithHTTPClient sends the discovery and key requests through client, for a proxy, instrumentation or a private certificate authority. Redirects are never followed, whatever the client says.
func WithJWKS ¶
WithJWKS verifies with a static JSON Web Key Set instead of fetching one: no network is involved, and rotating the keys is up to the application, which builds a new verifier when they change. It excludes WithJWKSURI.
func WithJWKSMaxAge ¶
WithJWKSMaxAge sets how long fetched keys are trusted. Past it they are refreshed, and never used again if the refresh fails. The default is five minutes.
func WithJWKSURI ¶
WithJWKSURI fetches the keys from uri, an HTTPS URL, instead of discovering it from the issuer's OpenID configuration. It excludes WithJWKS.
func WithRequiredClaims ¶
WithRequiredClaims requires claims besides iss, aud and exp, which are always required. A claim that is null counts as missing.
func WithTimeout ¶
WithTimeout bounds the discovery and key requests of one verification, and the wait for another caller's refresh. The default is three seconds: keep it below the function's timeout, so that the function has time to answer when the identity provider does not.
func WithUnknownKIDCooldown ¶
WithUnknownKIDCooldown sets the minimum interval between two refreshes caused by a token whose kid is not in the key set, so that forged tokens cannot hammer the identity provider. The default is five minutes; zero refreshes on every unknown kid.
type Reason ¶
type Reason string
Reason is a stable, credential-free reason for a failure, fit for logs and metrics. The values are Powertools Python's AuthFailureReason.
const ( ReasonMissingToken Reason = "missing_token" ReasonInvalidToken Reason = "invalid_token" ReasonInvalidClaims Reason = "invalid_claims" ReasonTokenExpired Reason = "token_expired" ReasonInvalidSignature Reason = "invalid_signature" ReasonInsufficientScope Reason = "insufficient_scope" ReasonForbidden Reason = "forbidden" )
The reasons a token or a request is rejected.
type RequireOption ¶
type RequireOption interface {
// contains filtered or unexported methods
}
RequireOption configures Require.
func WithAuthorize ¶
func WithAuthorize(allow func(r *http.Request, claims *Claims) bool) RequireOption
WithAuthorize adds a policy of your own, run once the token is verified and the scopes are checked: the request goes on only when allow returns true, and is otherwise refused with 403.
func WithErrorResponse ¶
func WithErrorResponse(respond func(w http.ResponseWriter, r *http.Request, f Failure)) RequireOption
WithErrorResponse writes the response of a refused request in place of the default JSON {"message": "Unauthorized"} (or Forbidden, or Service Unavailable). It can log or count the failure, but the protected handler never runs. Nothing is logged unless it does so.
type ScopeOption ¶
type ScopeOption []string
ScopeOption is the option WithScopes returns: it configures Require and the Authorize functions alike.
func WithScopes ¶
func WithScopes(scopes ...string) ScopeOption
WithScopes requires every one of scopes in the token, read from its scope, scp or scopes claim. Each must be an OAuth scope token.
type Verifier ¶
type Verifier interface {
// Verify returns the claims of token, a JWT without the Bearer
// prefix, once its signature and claims are verified. Failures are
// *Error values.
Verify(ctx context.Context, token string) (*Claims, error)
// Prefetch fetches the signing keys ahead of the first Verify, during
// the function's initialization.
Prefetch(ctx context.Context) error
}
Verifier verifies access tokens. A *JWTVerifier verifies the tokens of one issuer, AnyOf routes between several, and the authtest package has one for tests. VerifyAuthorizationHeader, VerifyRequest, Require and the Authorize functions work with any of them.
func AnyOf ¶
func AnyOf(verifiers ...*JWTVerifier) (Verifier, error)
AnyOf returns a Verifier for functions that trust the access tokens of several issuers, each with its own verifier. A token goes to the verifier of its iss claim, read before anything is verified: it only selects one of the configured verifiers, which then verifies the whole token. A token from any other issuer is invalid, and causes no network request.
At least one verifier is required, and no two may trust the same issuer.
Example ¶
package main
import (
"context"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/auth"
)
const resource = "https://orders.example.com"
func main() {
corporate, err := auth.NewJWTVerifier("https://login.example.com/", []string{resource}, []auth.Algorithm{auth.RS256})
if err != nil {
panic(err)
}
cognito, err := auth.NewCognitoVerifier("eu-west-1_AbC123", "orders-client", []string{resource})
if err != nil {
panic(err)
}
verifier, err := auth.AnyOf(corporate, cognito)
if err != nil {
panic(err)
}
// A token from an issuer nobody configured is refused without a request.
_, err = verifier.Verify(context.Background(), "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V2aWwuZXhhbXBsZS5jb20vIn0.c2ln")
fmt.Println(err)
}
Output: invalid access token
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package authtest tests code protected by the auth package without real tokens, keys or network: the Go side of Powertools Python's mock_claims.
|
Package authtest tests code protected by the auth package without real tokens, keys or network: the Go side of Powertools Python's mock_claims. |