jwt

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 6 Imported by: 0

README

tinywasm/jwt

Isomorphic JWT (HS256) for the tinywasm ecosystem: the same code signs and verifies on the native backend and inside a WASM/edge binary (browser, Cloudflare Workers, goflare).

It exists so a consumer that only needs to verify a token does not have to import an entire auth stack (ORM, bcrypt, OAuth, a database driver) to do it.

import "github.com/tinywasm/jwt"

secret := []byte("a-256-bit-secret")

token, err := jwt.Sign(secret, jwt.NewClaims(userID, 3600)) // ttl in seconds
if err != nil {
    return err
}

claims, err := jwt.Verify(secret, token)
switch err {
case nil:
    use(claims.Sub)
case jwt.ErrTokenExpired:
    // not an attack: the session ended, ask for a new login
case jwt.ErrInvalidToken:
    // malformed or unauthentic — do not tell the caller which
}

Design

HS256 only. No algorithm negotiation. That is the security model, not a limitation.

Verify never reads the alg field of the token — it always recomputes HS256. Choosing the algorithm from a value carried inside the untrusted token is the classic alg-confusion vulnerability, and it is how {"alg":"none"} forgeries get accepted.

Claims is a closed struct (Sub, Exp, Iat), never a map[string]any bag.

The library refuses rather than returning something that merely looks fine:

Refused Why
empty secret HMAC over an empty key is valid math — it mints tokens anyone can forge
empty subject a token that authenticates nobody would let "" through as an identity
token without exp it is malformed, not eternal
any signature mismatch compared in constant time (crypto.HMACEqual)

ErrTokenExpired is deliberately distinct from ErrInvalidToken: expiry is not an attack, and the caller must be able to tell "log in again" from "this is a forgery". Every other failure collapses into ErrInvalidToken on purpose — distinguishing "bad signature" from "bad base64" tells an attacker where they stand.

Status

Signing and verifying are done and tested (native + WASM).

Not yet usable from a frontend that has no secret, and not yet proven to interoperate with other JWT implementations. Both, plus clock-skew tolerance and key rotation, are specified in docs/PLAN.md.

Testing

gotest          # both suites: native + wasm
gotest -tinygo  # compiles the WASM suite with TinyGo

See AGENTS.md for the constraints any change must respect.

Documentation

Overview

Package jwt signs and verifies JSON Web Tokens (HS256) isomorphically: the same code runs on the native backend and inside a WASM/edge binary.

The library is deliberately small and closed: HS256 only, one claim set, no algorithm negotiation. See docs/ARCHITECTURE.md for why.

Index

Constants

View Source
const DefaultTTL = 86400 // 24h, in seconds

DefaultTTL is the lifetime NewClaims uses when ttl <= 0.

Variables

View Source
var (
	// ErrInvalidToken covers every malformed or unauthentic token: wrong shape, bad
	// signature, undecodable payload. It is deliberately ONE error: telling
	// "bad signature" apart from "bad base64" tells an attacker where they stand.
	ErrInvalidToken = fmt.Err("jwt", "token", "invalid")

	// ErrTokenExpired is separate because it is NOT an attack: the caller must be able
	// to tell "your session ended, log in again" from "this token is a forgery".
	ErrTokenExpired = fmt.Err("jwt", "token", "expired")

	// ErrEmptySecret is a refusal, not a failure. HMAC over an empty key is valid math:
	// it produces a token that verifies. A zero-value config would therefore mint
	// tokens that ANYONE can forge, and nothing would ever look wrong.
	ErrEmptySecret = fmt.Err("jwt", "secret", "empty")

	// ErrEmptySubject: a token that authenticates nobody is never what the caller meant.
	ErrEmptySubject = fmt.Err("jwt", "subject", "empty")
)

Functions

func Sign added in v0.0.2

func Sign(secret []byte, c Claims) (string, error)

Sign returns a signed HS256 token. It refuses to mint a forgeable or meaningless token rather than handing back one that merely looks fine.

Types

type Claims added in v0.0.2

type Claims struct {
	Sub string // subject: who the token authenticates
	Exp int64  // expiry, unix seconds
	Iat int64  // issued at, unix seconds
}

Claims is the payload. Closed on purpose: the registered claims this ecosystem actually uses. No `map[string]any` bag — that is how JWT libraries grow holes.

func NewClaims added in v0.0.2

func NewClaims(subject string, ttl int) Claims

NewClaims builds a claim set valid for ttl seconds from now.

func Verify added in v0.0.2

func Verify(secret []byte, token string) (Claims, error)

Verify authenticates a token and returns its claims.

The `alg` field of the header is READ BY NOBODY, and that is the point: this verifier always recomputes HS256. Choosing the algorithm from a value carried inside the untrusted token is the classic alg-confusion vulnerability — it is how `{"alg":"none"}` forgeries get accepted. Do not "fix" this by parsing the header.

func (*Claims) DecodeFields added in v0.0.2

func (c *Claims) DecodeFields(r model.FieldReader)

func (Claims) EncodeFields added in v0.0.2

func (c Claims) EncodeFields(w model.FieldWriter)

func (Claims) IsNil added in v0.0.2

func (c Claims) IsNil() bool

Jump to

Keyboard shortcuts

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