Documentation
¶
Overview ¶
Package jwt mints and verifies the RS512 JSON Web Tokens that carry an auth.Identity from the forward-auth server to the backends behind it.
A Signer turns an identity into a short-lived token and serves its public key as a JWKS. A Verifier checks such tokens against that JWKS and turns them back into an identity, as a function (Verify) or as net/http middleware (Middleware). Both are safe for concurrent use.
Claims of a token (a field is omitted when empty):
iss, aud SignerOptions.Issuer, SignerOptions.Audience
iat, nbf, exp, jti now, now - 30 s, now + Lifetime, random UUID
sub, provider, user_id Identity.Subject, Provider, UserID (omitted for uuid.Nil)
preferred_username, name, email, picture Identity.Username, Name, Email, Picture
roles, realm_roles, client_roles, groups, teams Identity.Roles, OIDCRoles, OIDCClientRoles, Groups, Teams (JSON arrays)
permissions Identity.Permissions as text forms (["builds:write","*:read"])
act while impersonating (RFC 8693): {"sub", "provider", "user_id"}
of Identity.Impersonator
any other key of Identity.Claims copied, except nonce, at_hash, c_hash, auth_time, sid, azp,
realm_access, resource_access, act
Sign fails on an invalid permission and refuses an inactive identity or impersonator. Verify reads the same claims back, sets Active and puts all claims of the token into Identity.Claims; permission entries that are not strings or do not parse are dropped. An act claim becomes an active Impersonator with only Subject, Provider and UserID, so a check wrapped in authorization.Real sees no roles or permissions of it. Attributes and Data are not carried.
The package never logs and starts no goroutines: the Verifier refreshes its keys lazily inside Verify.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer mints RS512 tokens for identities and serves its public key as a JWKS. It is safe for concurrent use.
Example ¶
The forward-auth server signs the identity of every checked request and serves the public key.
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"github.com/govengers/auth"
"github.com/govengers/auth/jwt"
)
// examplePrivateKeyPEM stands in for the PEM key a deployment loads from its secret store.
func examplePrivateKeyPEM() ([]byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}), nil
}
func main() {
privateKey, err := examplePrivateKeyPEM()
if err != nil {
fmt.Println(err)
return
}
signer, err := jwt.NewSigner(&jwt.SignerOptions{PrivateKey: privateKey, Issuer: "https://auth.example.com", Audience: []string{"builds-api"}})
if err != nil {
fmt.Println(err)
return
}
serveMux := http.NewServeMux()
auth.RegisterRoutes(serveMux, []auth.Route{{Method: http.MethodGet, Path: "/auth/jwks", Handler: signer.JWKSHandler()}})
token, err := signer.Sign(&auth.Identity{Provider: "github", Subject: "583231", Username: "octocat", Active: true, Teams: []string{"org/team"}})
if err != nil {
fmt.Println(err)
return
}
recorder := httptest.NewRecorder()
serveMux.ServeHTTP(recorder, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/auth/jwks", nil))
fmt.Println(token != "", recorder.Code, recorder.Header().Get("Cache-Control"))
}
Output: true 200 public, max-age=300
func NewSigner ¶
func NewSigner(options *SignerOptions) (*Signer, error)
NewSigner parses the private key and precomputes the JWKS. The key ID is the RFC 7638 SHA-256 thumbprint of the public key, so it is stable across restarts and replicas sharing the key.
func (*Signer) JWKSHandler ¶
JWKSHandler serves the public key as {"keys":[{kty,n,e,kid,alg:"RS512",use:"sig"}]} with Cache-Control: public, max-age=300. The document is computed once by NewSigner.
func (*Signer) Sign ¶
Sign mints an RS512 token for identity: header kid and typ JWT; claims iss, aud, iat, nbf (now - 30 s), exp (now + Lifetime), a random jti, the identity's fields and every other key of its Claims (see the package claim table). An invalid permission is an error; an inactive identity, or one whose impersonator is inactive, is refused with auth.ErrForbidden (Reason auth.ReasonInactive), since Verify returns every identity as active. It does not modify identity.
type SignerOptions ¶
type SignerOptions struct {
PrivateKey []byte // required: PEM RSA private key, PKCS#1 or PKCS#8, at least 2048 bits
Issuer string // required: iss of every token, also what verifiers expect
Audience []string // optional aud
Lifetime time.Duration // default 5 min
}
SignerOptions configure a Signer.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier checks RS512 tokens against a JWKS URL and turns them into identities. It keeps no token cache and starts no goroutine: Verify refetches the JWKS lazily, after RefreshInterval or for an unknown key ID, at most once per minute; the refetch is not cancelled with the triggering request and takes at most 10 s. Meanwhile verifications with a known key ID go on with the current keys, those with an unknown one wait for it or for the end of their request. Keys older than MaximumKeyAge count as unknown, so a key the signer rotated out is not trusted forever while its JWKS is unreachable. It is safe for concurrent use.
Example ¶
A backend verifies the token the forward-auth server put into the Authorization header.
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"github.com/govengers/auth"
"github.com/govengers/auth/jwt"
)
// examplePrivateKeyPEM stands in for the PEM key a deployment loads from its secret store.
func examplePrivateKeyPEM() ([]byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}), nil
}
func main() {
privateKey, err := examplePrivateKeyPEM()
if err != nil {
fmt.Println(err)
return
}
signer, err := jwt.NewSigner(&jwt.SignerOptions{PrivateKey: privateKey, Issuer: "https://auth.example.com", Audience: []string{"builds-api"}})
if err != nil {
fmt.Println(err)
return
}
forwardAuth := httptest.NewServer(signer.JWKSHandler())
defer forwardAuth.Close()
verifier, err := jwt.NewVerifier(context.Background(), &jwt.VerifierOptions{JWKSURL: forwardAuth.URL, Issuer: "https://auth.example.com", Audience: "builds-api"})
if err != nil {
fmt.Println(err)
return
}
api := verifier.Middleware()(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity := auth.IdentityFrom(request.Context())
_, _ = fmt.Fprintln(writer, identity.Provider, identity.Username, identity.Permissions, identity.Permits("builds", auth.AccessRead)) // the recorder never fails a write
}))
token, err := signer.Sign(&auth.Identity{Provider: "apikey", Subject: "ci", Username: "ci", Active: true, Permissions: []auth.Permission{{Resource: "builds", Access: auth.AccessWrite}}})
if err != nil {
fmt.Println(err)
return
}
request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/items", nil)
request.Header.Set("Authorization", "Bearer "+token)
recorder := httptest.NewRecorder()
api.ServeHTTP(recorder, request)
fmt.Print(recorder.Code, " ", recorder.Body.String())
}
Output: 200 apikey ci [builds:write] true
func NewVerifier ¶
func NewVerifier(ctx context.Context, options *VerifierOptions) (*Verifier, error)
NewVerifier fetches the JWKS once and fails when it cannot (fail fast). ctx bounds that fetch only.
func (*Verifier) Middleware ¶
Middleware requires a valid token in HeaderName ("Bearer <token>"): the identity goes into the request context, a missing or invalid token is answered 401 JSON with WWW-Authenticate: Bearer (RFC 6750), a failed JWKS fetch 500. It reports one ActionAuthenticate outcome per request to the Observer.
func (*Verifier) Verify ¶
Verify checks the token's signature (with the JWKS key named by its kid, under the key's algorithm), iss, aud, exp (required), nbf and iat with 30 s clock skew, and returns the identity it carries: Active, the fields of the package claim table and Claims = all claims of the token. Claims of the wrong type are skipped. An invalid token is an *auth.Error{Kind: auth.ErrUnauthenticated, Code: "invalid_token"}; a failed JWKS fetch for an unknown kid is a plain error. A failed periodic refresh keeps the previous keys (Middleware reports it as a degraded outcome) until they are MaximumKeyAge old; from then on every token is a plain error until a fetch succeeds.
type VerifierOptions ¶
type VerifierOptions struct {
JWKSURL string // required: https URL (http only on a loopback host or with InsecureJWKSURL) of the signer's JWKS (at most 1 MiB), without user info; only its RSA keys with an RSA signature alg (RS*, PS*), a kid and use "sig" (or none) count
InsecureJWKSURL bool // allows a plain http JWKSURL on any host, e.g. the in-cluster http://forwardauth:8080/auth/jwks; only where the network path to it is trusted
Issuer string // required: expected iss
Audience string // required: aud must contain it
RefreshInterval time.Duration // default 1 h, at least 1 min; an unknown kid triggers a refetch at most once per minute
MaximumKeyAge time.Duration // default 24 h, at least RefreshInterval: keys stop being trusted this long after the last successful fetch
HTTPClient *http.Client // default 10 s timeout; a redirect fails the JWKS fetch
HeaderName string // default "Authorization", value "Bearer <token>"
Observer auth.Observer // optional; Middleware reports one ActionAuthenticate outcome per request
}
VerifierOptions configure a Verifier.