Help us shape the future of Go! Take the 2026 Go Developer Survey

jwt

package
v1.2.0 Latest Latest
Warning

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

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

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

func (s *Signer) JWKSHandler() http.Handler

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

func (s *Signer) Sign(identity *auth.Identity) (string, error)

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

func (v *Verifier) Middleware() func(http.Handler) http.Handler

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

func (v *Verifier) Verify(ctx context.Context, token string) (*auth.Identity, error)

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.

Jump to

Keyboard shortcuts

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