jwt

package module
v0.0.0-...-0bba2f3 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 20 Imported by: 0

README

jwt — go-ruby-jwt

Go Reference CI Coverage

A pure-Go (no cgo) reimplementation of Ruby's jwt gem (tracking release 3.2.0) — the JSON Web Token library. It encodes and decodes JWS (JSON Web Signature) compact-serialisation tokens byte-faithfully to the gem: given the same key, algorithm, payload and header, the deterministic algorithms produce identical tokens, and every algorithm cross-verifies with MRI in both directions — without any Ruby runtime.

It is the JWT backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-yaml (the Psych emitter/loader) and go-ruby-regexp (the Onigmo engine).

Algorithms

The whole surface is built on Go's standard crypto/*crypto/hmac (HS), crypto/rsa PKCS1v15 (RS) and PSS (PS), crypto/ecdsa (ES) — so it is CGO-free and dependency-free:

  • HS256 / HS384 / HS512 — HMAC
  • RS256 / RS384 / RS512 — RSA PKCS#1 v1.5
  • PS256 / PS384 / PS512 — RSA-PSS
  • ES256 / ES384 / ES512 — ECDSA
  • none — unsecured, per the JWA none algorithm

Registered claim verification (exp, nbf, iat, iss, aud, sub, jti) matches the gem's semantics, and JWK import/export is supported.

Usage

import "github.com/go-ruby-jwt/jwt"

tok, _ := jwt.Encode(map[string]any{"user": "amy", "exp": exp}, secret, "HS256", nil)

payload, header, _ := jwt.Decode(tok, secret, true, jwt.Options{
	Algorithms: []string{"HS256"},
})

Tests & coverage

go test ./... runs the unit and differential-oracle suites (cross-verified against MRI's jwt gem). The CI gate enforces 100% statement coverage and builds/tests on all six 64-bit Go targets — amd64, arm64, riscv64, loong64, ppc64le, s390x.

License

BSD-3-Clause. Copyright (c) the go-ruby-jwt/jwt authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package jwt is a pure-Go (no cgo) reimplementation of Ruby's `jwt` gem — the JSON Web Token library. It encodes and decodes JWS (JSON Web Signature) compact-serialisation tokens, byte-faithfully to the gem: given the same key, algorithm, payload and header, deterministic algorithms (HS*, RS*, PS-with- verification, none) produce identical tokens, and every algorithm (HS/RS/ES/PS) cross-verifies with MRI in both directions.

The whole surface is built on Go's standard crypto/* — crypto/hmac (HS), crypto/rsa PKCS1v15 (RS) and PSS (PS), crypto/ecdsa (ES) — so it is CGO-free and dependency-free.

tok, _ := jwt.Encode(map[string]any{"user": "amy", "exp": exp}, secret, "HS256", nil)
payload, header, _ := jwt.Decode(tok, secret, true, jwt.Options{Algorithms: []string{"HS256"}})

It is the JWT backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-yaml (Psych) and go-ruby-regexp (Onigmo).

Index

Constants

View Source
const Version = "3.2.0"

Version mirrors the JWT::VERSION::STRING constant the gem exposes; it is the gem release this port tracks.

Variables

View Source
var (
	// ErrEncode is JWT::EncodeError — a token could not be produced.
	ErrEncode = errors.New("JWT::EncodeError")

	// ErrDecode is JWT::DecodeError, the root of every decode-time failure.
	ErrDecode = errors.New("JWT::DecodeError")

	// ErrVerification is JWT::VerificationError — the signature did not verify.
	ErrVerification = newParent("JWT::VerificationError", ErrDecode)
	// ErrIncorrectAlgorithm is JWT::IncorrectAlgorithm — the token's alg is not
	// among those the caller allowed (alg-confusion guard).
	ErrIncorrectAlgorithm = newParent("JWT::IncorrectAlgorithm", ErrDecode)
	// ErrExpiredSignature is JWT::ExpiredSignature — the exp claim is in the past.
	ErrExpiredSignature = newParent("JWT::ExpiredSignature", ErrDecode)
	// ErrImmatureSignature is JWT::ImmatureSignature — nbf is still in the future.
	ErrImmatureSignature = newParent("JWT::ImmatureSignature", ErrDecode)
	// ErrInvalidIat is JWT::InvalidIatError — the iat claim is malformed or future.
	ErrInvalidIat = newParent("JWT::InvalidIatError", ErrDecode)
	// ErrInvalidIssuer is JWT::InvalidIssuerError — the iss claim did not match.
	ErrInvalidIssuer = newParent("JWT::InvalidIssuerError", ErrDecode)
	// ErrInvalidAud is JWT::InvalidAudError — the aud claim did not match.
	ErrInvalidAud = newParent("JWT::InvalidAudError", ErrDecode)
	// ErrInvalidSub is JWT::InvalidSubError — the sub claim did not match.
	ErrInvalidSub = newParent("JWT::InvalidSubError", ErrDecode)
	// ErrInvalidJti is JWT::InvalidJtiError — the jti claim failed validation.
	ErrInvalidJti = newParent("JWT::InvalidJtiError", ErrDecode)
	// ErrInvalidPayload is JWT::InvalidPayload — a reserved claim has a bad type.
	ErrInvalidPayload = newParent("JWT::InvalidPayload", ErrDecode)
	// ErrMissingRequiredClaim is JWT::MissingRequiredClaim — a required claim is
	// absent from the payload.
	ErrMissingRequiredClaim = newParent("JWT::MissingRequiredClaim", ErrDecode)
	// ErrBase64Decode is JWT::Base64DecodeError — a segment was not valid base64url.
	ErrBase64Decode = newParent("JWT::Base64DecodeError", ErrDecode)
	// ErrJWK is JWT::JWKError — a JWK/JWKS could not be parsed or materialised into
	// a usable public key (malformed JSON, missing or bad key material, an
	// unsupported key type or curve). Like the gem's JWKError it is a DecodeError.
	ErrJWK = newParent("JWT::JWKError", ErrDecode)
)

The sentinels below are the gem's exception classes. Match a specific one with errors.Is(err, jwt.ErrExpiredSignature), or the whole decode family with errors.Is(err, jwt.ErrDecode).

Functions

func Decode

func Decode(token string, key any, verify bool, opts Options) (payload any, header any, err error)

Decode parses and (optionally) verifies a JWS compact token, mirroring the gem's JWT.decode(token, key, verify, options) => [payload, header]. It returns the decoded payload and header, both as *OrderedMap (so key order round-trips).

When verify is false the signature and claims are not checked and key/options may be nil/zero — the gem's "no verification" mode. When verify is true the signature is checked against key using an algorithm from opts.Algorithms (which must be non-empty), and the exp/nbf/iat/iss/aud/sub/jti/required-claim rules the options enable are applied.

func Encode

func Encode(payload any, key any, algorithm string, headerFields any) (string, error)

Encode builds a signed JWS compact token from a payload, mirroring the gem's JWT.encode(payload, key, algorithm, header_fields).

The header is header_fields with "alg" set to algorithm: if header_fields already carries an "alg" key its position (and value) is kept, otherwise "alg" is appended last — exactly the gem's ordering. The header and payload are serialised with order-preserving compact JSON, base64url-encoded (no padding), joined with ".", signed, and the base64url signature appended.

key is the signing key for the chosen algorithm: an HMAC secret (string / []byte) for HS*, an *rsa.PrivateKey for RS*/PS*, an *ecdsa.PrivateKey for ES*. For the unsecured "none" algorithm key is ignored and the signature is empty.

payload and header_fields may be an *OrderedMap (to pin order), a map[string]any, or nil (header only — header_fields nil means just {"alg":...}).

Types

type Error

type Error struct {
	// Kind names the gem exception class (e.g. "JWT::ExpiredSignature").
	Kind string
	// Message is the human-readable text, byte-for-byte the gem's message.
	Message string
	// contains filtered or unexported fields
}

Error is the shared interface of every error this package raises. It mirrors the gem's exception hierarchy, whose root is JWT::DecodeError < StandardError: a caller can match a single kind with errors.As, or the whole family with errors.Is(err, jwt.ErrDecode).

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the parent sentinel so errors.Is(err, ErrDecode) matches any decode-time error, just as `rescue JWT::DecodeError` catches the whole family.

type JWK

type JWK struct {
	// Kty is "RSA" or "EC".
	Kty string
	// Crv is the EC curve name ("P-256"/"P-384"/"P-521"), empty for RSA.
	Crv string
	// N, E are the base64url RSA modulus and exponent (RSA only).
	N, E string
	// X, Y are the base64url EC coordinates (EC only).
	X, Y string
	// Kid is the key id. For a JWK built locally with NewJWK it is the gem's
	// default key_digest (see below); for a JWK read from JSON with ParseJWK /
	// ParseJWKS it is the provider-assigned "kid" carried on the wire, used to
	// select the key by JWKS.Find / JWKS.Select.
	Kid string
	// Alg is the intended JWS algorithm ("RS256", "ES256", …) when the JWK
	// declares one, or "" otherwise. Only populated by ParseJWK / ParseJWKS.
	Alg string
	// Use is the intended public-key use ("sig"/"enc") when declared, or "".
	// Only populated by ParseJWK / ParseJWKS.
	Use string
	// contains filtered or unexported fields
}

JWK is a JSON Web Key (RFC 7517) for an RSA or EC public key — the export/import half of the gem's JWT::JWK. It mirrors the gem's export byte-for-byte: an RSA key exports {"kty":"RSA","n":..,"e":..,"kid":..}, an EC key {"kty":"EC","crv":..,"x":..,"y":..,"kid":..}.

Kid is the gem's default key id (JWK#kid), which is NOT the RFC 7638 thumbprint: the gem derives it as the lowercase-hex SHA-256 of the DER of an ASN.1 SEQUENCE of the key's two defining integers — (n, e) for RSA, (x, y) for EC — matching JWT::JWK's key_digest byte-for-byte. The RFC 7638 thumbprint (base64url) is available separately via Thumbprint.

func NewJWK

func NewJWK(key any) (*JWK, error)

NewJWK builds a JWK from an RSA or EC key (public, or the public half of a private key), mirroring JWT::JWK.new. The thumbprint Kid is computed on build.

func ParseJWK

func ParseJWK(data []byte) (*JWK, error)

ParseJWK reads a single JSON Web Key from JSON and materialises its RSA (n/e) or EC (crv/x/y) public key, mirroring JWT::JWK.import for a lone key. The returned JWK carries the provider's "kid"/"alg"/"use" (for later selection) and the parsed public key (reachable via PublicKey). An unsupported key type is an error — unlike ParseJWKS, which skips a key it cannot use.

func (*JWK) Export

func (j *JWK) Export() *OrderedMap

Export renders the JWK as an ordered object, key-for-key as the gem's JWK#export: RSA → kty,n,e,kid; EC → kty,crv,x,y,kid.

func (*JWK) PublicKey

func (j *JWK) PublicKey() any

PublicKey returns the parsed crypto public key the JWK wraps, for verification.

func (*JWK) Thumbprint

func (j *JWK) Thumbprint() string

Thumbprint is the RFC 7638 key thumbprint: the unpadded base64url SHA-256 of the key's canonical JSON (members sorted, compact), byte-for-byte the gem's JWT::JWK::Thumbprint. It is distinct from Kid (the gem's default key_digest).

type JWKS

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

JWKS is a set of JWKs, the key-lookup input to Decode for a token whose header carries a "kid" (the gem's JWT::JWK::Set). VerifyKey resolves the kid.

func NewJWKS

func NewJWKS(keys ...*JWK) *JWKS

NewJWKS builds a key set.

func ParseJWKS

func ParseJWKS(data []byte) (*JWKS, error)

ParseJWKS reads a JSON Web Key Set ({"keys":[...]}) from JSON, materialising each RSA/EC key into a public key and keying it by the provider's "kid". A key whose type this library cannot use for JWS verification (e.g. an "oct" symmetric key) is skipped rather than failing the whole set, matching a client that ignores keys it has no use for; a key of a supported type with bad material is an error.

func (*JWKS) Find

func (s *JWKS) Find(kid string) *JWK

Find returns the JWK with the given kid, or nil.

func (*JWKS) Keys

func (s *JWKS) Keys() []*JWK

Keys returns the set's keys in document order. The returned slice must not be mutated; it lets a consumer (e.g. an OIDC key set) adapt each imported JWK — its provider-assigned kid/alg/use and materialised public key — into its own representation.

func (*JWKS) Select

func (s *JWKS) Select(kid, alg string) (*JWK, error)

Select resolves a verification key from the set by kid and (fallback) alg, mirroring how a client picks a JWKS key: a non-empty kid selects that exact key; an empty kid falls back to the sole signing-capable key whose type serves alg, rejecting an ambiguous (or empty) set. It is OIDC-agnostic — no issuer/audience logic — so any JWS consumer can reuse it.

type Options

type Options struct {
	// Algorithms is the allow-list of acceptable "alg" header values (the gem's
	// algorithm:/algorithms: options). A token whose alg is not listed is rejected
	// with IncorrectAlgorithm — the alg-confusion guard. Empty (with verify=true)
	// is an error, matching the gem's "An algorithm must be specified".
	Algorithms []string

	// Leeway is the clock-skew allowance (seconds) applied to exp, nbf and iat.
	Leeway int64
	// ExpLeeway / NbfLeeway override Leeway for the respective claim when non-nil.
	ExpLeeway *int64
	NbfLeeway *int64

	// VerifyExpiration toggles the exp check; the gem defaults it on, so this
	// package treats it as on unless explicitly disabled via VerifyExpirationSet.
	VerifyExpiration    bool
	VerifyExpirationSet bool // distinguishes "false" from "unset (=> true)"

	// VerifyNotBefore toggles the nbf check (gem default on; same set-flag rule).
	VerifyNotBefore    bool
	VerifyNotBeforeSet bool

	// VerifyIat, when true, rejects a payload whose iat is malformed or in the
	// future (the gem's verify_iat:).
	VerifyIat bool

	// Issuer + VerifyIss check the iss claim. Issuer may be a single string or a
	// []string of acceptable issuers.
	Issuer    any
	VerifyIss bool

	// Audience + VerifyAud check the aud claim. Either side may be a string or a
	// []string; the check passes when they intersect.
	Audience  any
	VerifyAud bool

	// Subject + VerifySub check the sub claim against an expected string.
	Subject   string
	VerifySub bool

	// VerifyJti, when true, requires the jti claim to be present and non-empty.
	// A JtiValidator, when set, replaces that default with a custom predicate.
	VerifyJti    bool
	JtiValidator func(jti any) bool

	// RequiredClaims lists claim names that must be present in the payload
	// (the gem's required_claims:).
	RequiredClaims []string
}

Options mirrors the decode-options Hash the gem accepts as JWT.decode's fourth argument. The zero value verifies nothing beyond the signature and the always-on exp/nbf checks (which the gem also runs by default) — set the Verify* flags and their companion expected values to switch on claim validation.

type OrderedMap

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

OrderedMap is an insertion-ordered string-keyed object, the analogue of a Ruby Hash. Encode accepts one as payload or header so a caller can pin claim / header order; Decode returns payload and header as *OrderedMap so key order round-trips.

func NewOrderedMap

func NewOrderedMap() *OrderedMap

NewOrderedMap returns an empty ordered object.

func (*OrderedMap) Get

func (m *OrderedMap) Get(key string) (any, bool)

Get returns the value stored under key and whether it was present.

func (*OrderedMap) Keys

func (m *OrderedMap) Keys() []string

Keys returns the keys in insertion order.

func (*OrderedMap) Len

func (m *OrderedMap) Len() int

Len reports the number of entries.

func (*OrderedMap) Set

func (m *OrderedMap) Set(key string, val any)

Set stores val under key, appending the key on first insertion and preserving its position on update (Ruby Hash#[]= semantics).

Jump to

Keyboard shortcuts

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