jsonwt

package module
v0.0.0-...-ec631fe Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 9 Imported by: 1

README

jsonwt

jsonwt is a small, dependency-free Go package for issuing and consuming signed, JWT-shaped tokens in local tools, demos, tests, and trusted internal prototypes. It intentionally provides a narrow API and token format rather than implementing the JWT standard.

Warning: Do not use jsonwt for production authentication, authorization, sessions, or across an untrusted network boundary. Tokens are signed but not encrypted, and the package does not provide key management, revocation, replay protection, or standards compatibility. Read Security and scope before choosing it.

Install

In a Go module, run:

go get github.com/mdhender/jsonwt@latest

The module supports Go 1.17 and later.

Start here

Follow the tutorial and concise user manual to create an HS256 signer, issue a token with an application claim, parse and validate the token, and extract the claim.

Documentation

License

jsonwt is available under the MIT License.

Documentation

Overview

Package jsonwt issues and verifies small, signed tokens for local tools, tests, demos, and trusted prototypes.

Tokens use the familiar header.payload.signature shape, but their fields and claim representation are package-specific. This package is not a general RFC 7519 JWT implementation and its tokens are not promised to interoperate with JWT libraries. Token contents are encoded, not encrypted.

The normal lifecycle is to create a Factory from a Signer, issue a signed Token with Factory.Token, transport Token.String, and consume it with Factory.Parse. Decode and the HTTP extraction helpers do not verify tokens.

The v1 API supports Go 1.17 and later. Exported API behavior and the documented token wire format are compatibility commitments throughout v1; incompatible changes require a new major version.

Example
package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

func main() {
	type applicationClaim struct {
		User  string   `json:"user"`
		Roles []string `json:"roles"`
	}

	signer, err := signers.NewHS256([]byte("local-demo-secret-change-me"))
	if err != nil {
		panic(err)
	}
	factory := jsonwt.NewFactory("tutorial-key", signer)

	claim := applicationClaim{
		User:  "ada",
		Roles: []string{"reader", "writer"},
	}
	token, err := factory.Token(15*time.Minute, claim)
	if err != nil {
		panic(err)
	}
	encoded := token.String()
	fmt.Println("token sections:", len(strings.Split(encoded, ".")))

	parsed, err := factory.Parse(encoded)
	if err != nil {
		panic(err)
	}
	var decoded applicationClaim
	if err := parsed.Claim(&decoded); err != nil {
		panic(err)
	}

	fmt.Println("user:", decoded.User)
	fmt.Println("roles:", decoded.Roles)

}
Output:
token sections: 3
user: ada
roles: [reader writer]
Example (BearerToken)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

type sessionClaim struct {
	User string   `json:"user"`
	Tags []string `json:"tags"`
}

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	factory := newFactory("local-key", "local-demo-secret-change-me")
	issued, err := factory.Token(10*time.Minute, sessionClaim{User: "ada"})
	if err != nil {
		panic(err)
	}

	request := httptest.NewRequest(http.MethodGet, "/profile", nil)
	request.Header.Set("Authorization", "Bearer "+issued.String())

	received := jsonwt.FromBearerToken(request)
	if received == nil {
		panic("missing bearer token")
	}
	if err := factory.Validate(received); err != nil {
		panic(err)
	}
	fmt.Println(received.IsValid())

}
Output:
true
Example (Context)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

type sessionClaim struct {
	User string   `json:"user"`
	Tags []string `json:"tags"`
}

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	factory := newFactory("local-key", "local-demo-secret-change-me")
	issued, err := factory.Token(10*time.Minute, sessionClaim{User: "ada"})
	if err != nil {
		panic(err)
	}
	validated, err := factory.Parse(issued.String())
	if err != nil {
		panic(err)
	}

	request := httptest.NewRequest(http.MethodGet, "/profile", nil)
	ctx := validated.NewContext(request.Context())
	request = request.WithContext(ctx)

	received, ok := jsonwt.FromContext(request.Context())
	if !ok {
		panic("token missing from context")
	}
	fmt.Println(received == validated)

}
Output:
true
Example (Cookies)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

type sessionClaim struct {
	User string   `json:"user"`
	Tags []string `json:"tags"`
}

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	factory := newFactory("local-key", "local-demo-secret-change-me")
	issued, err := factory.Token(10*time.Minute, sessionClaim{User: "ada"})
	if err != nil {
		panic(err)
	}

	response := httptest.NewRecorder()
	issued.SetCookie(response)
	cookie := response.Result().Cookies()[0]

	request := httptest.NewRequest(http.MethodGet, "/profile", nil)
	request.AddCookie(cookie)
	received := jsonwt.FromCookie(request)
	if received == nil {
		panic("missing token cookie")
	}
	if err := factory.Validate(received); err != nil {
		panic(err)
	}

	response = httptest.NewRecorder()
	jsonwt.DeleteCookie(response)
	deleted := response.Result().Cookies()[0]
	fmt.Println(cookie.Name, cookie.HttpOnly, deleted.MaxAge)

}
Output:
jsonwt true -1
Example (CustomClaims)
package main

import (
	"fmt"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

type sessionClaim struct {
	User string   `json:"user"`
	Tags []string `json:"tags"`
}

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	factory := newFactory("local-key", "local-demo-secret-change-me")
	issued, err := factory.Token(10*time.Minute, sessionClaim{
		User: "ada",
		Tags: []string{"reader", "tester"},
	})
	if err != nil {
		panic(err)
	}

	parsed, err := factory.Parse(issued.String())
	if err != nil {
		panic(err)
	}
	var claim sessionClaim
	if err := parsed.Claim(&claim); err != nil {
		panic(err)
	}
	fmt.Println(claim.User, claim.Tags)

}
Output:
ada [reader tester]
Example (Errors)
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	factory := newFactory("local-key", "local-demo-secret-change-me")

	_, err := factory.Parse("not-a-token")
	fmt.Println(errors.Is(err, jsonwt.ErrBadToken))

	_, err = factory.Token(0, nil)
	fmt.Println(errors.Is(err, jsonwt.ErrInvalid))

	_, err = factory.Parse(expiredToken())
	fmt.Println(errors.Is(err, jsonwt.ErrInvalid))

}

func expiredToken() string {
	header := base64.RawURLEncoding.EncodeToString([]byte(`{"ver":1,"alg":"HS256","typ":"JWT","kid":"local-key"}`))
	payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":2,"iat":1}`))
	message := header + "." + payload
	mac := hmac.New(sha256.New, []byte("local-demo-secret-change-me"))
	_, _ = mac.Write([]byte(message))
	return message + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}
Output:
true
true
true
Example (KeyRotation)
package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

func newFactory(keyID, secret string) *jsonwt.Factory {
	signer, err := signers.NewHS256([]byte(secret))
	if err != nil {
		panic(err)
	}
	return jsonwt.NewFactory(keyID, signer)
}

func main() {
	oldFactory := newFactory("local-key-1", "old-local-secret")
	oldToken, err := oldFactory.Token(10*time.Minute, nil)
	if err != nil {
		panic(err)
	}

	newFactory := newFactory("local-key-2", "new-local-secret")
	_, err = newFactory.Parse(oldToken.String())
	fmt.Println(errors.Is(err, jsonwt.ErrUnauthorized))

	newToken, err := newFactory.Token(10*time.Minute, nil)
	if err != nil {
		panic(err)
	}
	_, err = newFactory.Parse(newToken.String())
	fmt.Println(err == nil)

}
Output:
true
true
Example (TestClock)
package main

import (
	"fmt"
	"time"

	"github.com/mdhender/jsonwt"
	"github.com/mdhender/jsonwt/signers"
)

type fixedClock struct {
	now time.Time
}

func (c *fixedClock) Now() time.Time { return c.now }

func main() {
	signer, err := signers.NewHS256([]byte("test-secret"))
	if err != nil {
		panic(err)
	}
	clock := &fixedClock{now: time.Unix(1_700_000_000, 0)}
	factory := jsonwt.NewFactoryWithClock("test-key", signer, clock)

	token, err := factory.Token(time.Minute, nil)
	if err != nil {
		panic(err)
	}

	clock.now = clock.now.Add(time.Minute)
	fmt.Println(token.IsValid())

}
Output:
false

Index

Examples

Constants

View Source
const (
	// ErrBadFactory indicates a nil Factory, empty factory key ID, nil Signer,
	// or nil Clock.
	ErrBadFactory = Error("bad factory")
	// ErrBadToken indicates malformed compact framing, base64, JSON, or signature
	// encoding. Claim returns ErrBadToken for a nil Token.
	ErrBadToken = Error("bad token")
	// ErrInvalid indicates an invalid requested lifetime, a nil Token passed to
	// Sign or Validate, or a token that is unsigned, not yet active, expired, or
	// missing its required iat or exp time.
	ErrInvalid = Error("invalid token")
	// ErrMissingClaim indicates that a valid Token has no application claim.
	ErrMissingClaim = Error("missing claim")
	// ErrUnauthorized indicates an algorithm, key ID, or signature mismatch.
	ErrUnauthorized = Error("unauthorized")
)

Variables

This section is empty.

Functions

func DeleteCookie

func DeleteCookie(w http.ResponseWriter)

DeleteCookie writes a deletion cookie named "jsonwt". The cookie has an empty value, Path "/", HttpOnly true, Expires at Unix second 1, and MaxAge -1. Domain, Secure, and SameSite retain their net/http zero values. w must be non-nil.

func SetCookie

func SetCookie(w http.ResponseWriter, t *Token)

SetCookie writes t in a cookie named "jsonwt". For a usable future exp, the cookie has Path "/", Value t.String(), Expires equal to exp, HttpOnly true, and MaxAge equal to the whole seconds remaining at call time. It uses the Factory clock associated with t, or the system clock when t has no associated clock. Domain, Secure, and SameSite retain their net/http zero values.

SetCookie writes the deletion cookie described by DeleteCookie when t is nil, exp is missing or elapsed, or less than one whole second remains. It does not otherwise validate t. w must be non-nil.

Types

type Clock

type Clock interface {
	Now() time.Time
}

Clock supplies the current time used to issue and validate tokens. Clock implementations may return a time in any location; Factory converts it to UTC before use.

type Error

type Error string

Error is a comparable constant error value. Package sentinel errors can be tested with errors.Is, including when an operation adds context by wrapping.

func (Error) Error

func (e Error) Error() string

Error returns the error message.

type Factory

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

Factory creates, signs, parses, and validates Tokens with one key ID and one Signer. A Factory does not discover keys or select among multiple signers.

func NewFactory

func NewFactory(kid string, s Signer) *Factory

NewFactory returns a Factory that identifies and signs tokens with kid and s and uses the system clock. It always returns a non-nil Factory; configuration validation is deferred to Sign, Token, Parse, and Validate, which return ErrBadFactory when kid is empty or s is nil. Factories are cheap and contain no key-discovery or key-ring behavior, so callers rotate keys by creating and selecting a new Factory.

func NewFactoryWithClock

func NewFactoryWithClock(kid string, s Signer, clock Clock) *Factory

NewFactoryWithClock returns a Factory that uses clock to issue and validate tokens. It is intended for deterministic tests; production callers should normally use NewFactory. A nil clock makes the Factory invalid, causing Sign, Token, Parse, and Validate to return ErrBadFactory.

func (*Factory) ID

func (f *Factory) ID() string

ID returns the key ID supplied to NewFactory. A nil Factory has an empty ID.

func (*Factory) Parse

func (f *Factory) Parse(data string) (*Token, error)

Parse decodes data, then validates its algorithm, key ID, signature, and lifetime. On success it returns a verified Token. On every error it returns a nil Token.

Malformed compact data or signature encoding returns ErrBadToken. An algorithm, key ID, or signature mismatch returns ErrUnauthorized. An inactive, expired, or incomplete lifetime returns ErrInvalid. A malformed input is rejected before factory configuration is checked; otherwise a nil or misconfigured Factory returns ErrBadFactory. Signer errors are propagated.

func (*Factory) Sign

func (f *Factory) Sign(t *Token) error

Sign encodes and signs t. It sets the token's alg and kid fields from the Factory, regenerates the encoded header and payload, and replaces any prior signature. Calling Sign repeatedly is safe.

Sign marks t as signed after the Signer succeeds, but does not check its time validity. It returns ErrBadFactory for a nil or misconfigured Factory, ErrInvalid for a nil Token, and propagates JSON and Signer errors.

func (*Factory) Token

func (f *Factory) Token(ttl time.Duration, claim interface{}) (*Token, error)

Token creates a Token with NewToken and signs it with the Factory. ttl and claim have the semantics documented by NewToken. Token returns ErrBadFactory before examining ttl or claim when the Factory is nil or misconfigured. It otherwise returns errors from NewToken or Sign unchanged.

func (*Factory) Validate

func (f *Factory) Validate(t *Token) error

Validate verifies t's alg and kid against the Factory, decodes and compares its signature in constant time, and then checks its lifetime using the Factory clock. Successful validation marks t as signed and associates the Factory clock with it so IsValid and Claim use the same clock.

A nil Token or invalid lifetime returns ErrInvalid. For a non-nil Token, a nil or misconfigured Factory returns ErrBadFactory. A malformed signature encoding returns ErrBadToken; metadata or signature mismatches return ErrUnauthorized. Signer errors are propagated. Validate always clears any prior signed state before checking the token.

type Signer

type Signer interface {
	// Algorithm returns the token header's alg value, for example "HS256".
	Algorithm() string
	// Sign returns the signature of msg. A Factory passes the exact bytes of
	// the encoded header, a period, and the encoded payload.
	Sign(msg []byte) ([]byte, error)
}

Signer supplies the algorithm identifier and message authentication used by a Factory. Implementations must return the same signature for the same message and key so Factory.Validate can compare signatures.

type Token

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

Token is a package-specific signed token. Its fields are intentionally opaque; use a Factory to create, sign, parse, and validate tokens, and use the Token methods to inspect their encoded sections and application claim.

A Token returned by NewToken is unsigned. A Token returned by Decode or an HTTP extraction helper is unverified. Factory.Token returns a signed token, and Factory.Parse returns a decoded token only after successful validation.

func Decode

func Decode(data string) (*Token, error)

Decode parses data in header.payload.signature form without verifying it. Exactly three non-empty sections are required. Decode raw-URL-base64 decodes the header and payload, requires each to be a JSON object, and unmarshals the package fields. It preserves all three original encoded sections verbatim.

Decode does not decode the signature, compare a signature, check factory metadata, or check token times. A successful result is therefore untrusted and IsValid returns false until Factory.Validate succeeds. Any framing, header, or payload error returns a nil Token and an error matching ErrBadToken. Callers should normally use Factory.Parse instead.

func FromBearerToken

func FromBearerToken(r *http.Request) *Token

FromBearerToken decodes the Token in r's Authorization header. The header must contain exactly two whitespace-separated fields; the first must be the Bearer scheme, matched case-insensitively, and the second must be a compact token accepted by Decode.

FromBearerToken does not verify the token. Callers must use Factory.Validate before use. It returns nil when r is nil or the header or token is malformed.

func FromContext

func FromContext(ctx context.Context) (*Token, bool)

FromContext returns the non-nil Token stored by Token.NewContext. It returns nil, false for a nil context, a missing value, a value of another type, or a stored nil Token. FromContext does not validate the Token.

func FromCookie

func FromCookie(r *http.Request) *Token

FromCookie decodes the Token in r's cookie named "jsonwt". It does not verify the token; callers must use Factory.Validate before use. It returns nil when r is nil or the cookie is missing or contains a malformed token.

func FromRequest

func FromRequest(r *http.Request) *Token

FromRequest returns a decoded Token from r. A syntactically decodable bearer token takes precedence over the "jsonwt" cookie, even if later validation fails. If the bearer header is absent or malformed, FromRequest falls back to the cookie. It returns nil for a nil request or when neither source decodes. Callers must use Factory.Validate before using the returned token.

func NewToken

func NewToken(ttl time.Duration, claim interface{}) (*Token, error)

NewToken returns an unsigned Token issued at the current UTC time. The caller must use Factory.Sign before transporting or using it.

ttl must be positive. NewToken records iat and exp as whole Unix seconds and returns ErrInvalid when ttl is zero or negative. A positive ttl shorter than one second can truncate to the same iat and exp and therefore produce an immediately invalid token; callers should use at least one second.

If claim is non-nil, NewToken marshals it as JSON and stores its unpadded raw-URL-base64 representation in the package-specific claim field. JSON marshal errors are returned unchanged. A nil claim omits that field.

func (*Token) Claim

func (t *Token) Claim(v interface{}) error

Claim decodes the application claim as JSON into v. The Token must currently be valid, and v must satisfy the same requirements as json.Unmarshal, normally a non-nil pointer.

Claim returns ErrBadToken for a nil receiver, ErrInvalid for an unsigned or time-invalid Token, and ErrMissingClaim when the claim field is absent. Claim base64 and JSON decoding errors, including invalid destination errors, are returned unchanged.

func (*Token) DeleteCookie

func (t *Token) DeleteCookie(w http.ResponseWriter)

DeleteCookie writes the package's deletion cookie. The receiver may be nil; it is not inspected. w must be non-nil.

func (*Token) HasClaim

func (t *Token) HasClaim() bool

HasClaim reports whether the package-specific claim field is present. It does not validate the Token or decode the claim. A nil Token returns false.

func (*Token) Header

func (t *Token) Header() string

Header returns the token's unpadded raw-URL-base64 header section. It returns an empty string for a nil Token or an unsigned Token not yet encoded by Sign.

func (*Token) IsValid

func (t *Token) IsValid() bool

IsValid reports whether the Token has a successfully generated or verified signature and is valid at the current time. Tokens successfully created or validated by a Factory use that Factory's clock; other tokens use the system clock. A nil or unsigned Token, or one with a zero iat or exp, is invalid. The iat and optional nbf boundaries are inclusive: now >= iat and now >= nbf. The exp boundary is exclusive: now < exp. A zero nbf imposes no additional boundary.

func (*Token) NewContext

func (t *Token) NewContext(ctx context.Context) context.Context

NewContext returns a child context carrying t under a package-private key. A nil parent is treated as context.Background. NewContext does not validate t. A nil Token receiver may be stored, but FromContext reports it as absent.

func (*Token) Payload

func (t *Token) Payload() string

Payload returns the token's unpadded raw-URL-base64 payload section. It returns an empty string for a nil Token or an unsigned Token not yet encoded by Sign.

func (*Token) SetCookie

func (t *Token) SetCookie(w http.ResponseWriter)

SetCookie writes the Token using the package cookie contract documented by the package-level SetCookie function. A nil receiver writes a deletion cookie. w must be non-nil.

func (*Token) Signature

func (t *Token) Signature() string

Signature returns the token's signature section verbatim. Factory.Sign produces unpadded raw-URL-base64; Decode preserves any non-empty signature text for later validation. Signature returns an empty string for a nil Token or a newly created Token not yet signed.

func (*Token) String

func (t *Token) String() string

String returns the compact header.payload.signature representation and implements fmt.Stringer. It returns an empty string for a nil Token. A Token returned by NewToken has empty encoded sections until Factory.Sign is called and must not be transported before then.

Directories

Path Synopsis
cmd
server command
Package main implements a non-production server for testing the JSONWT API.
Package main implements a non-production server for testing the JSONWT API.
Package signers provides the built-in jsonwt.Signer implementation.
Package signers provides the built-in jsonwt.Signer implementation.

Jump to

Keyboard shortcuts

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