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 ¶
- Constants
- func DeleteCookie(w http.ResponseWriter)
- func SetCookie(w http.ResponseWriter, t *Token)
- type Clock
- type Error
- type Factory
- type Signer
- type Token
- func (t *Token) Claim(v interface{}) error
- func (t *Token) DeleteCookie(w http.ResponseWriter)
- func (t *Token) HasClaim() bool
- func (t *Token) Header() string
- func (t *Token) IsValid() bool
- func (t *Token) NewContext(ctx context.Context) context.Context
- func (t *Token) Payload() string
- func (t *Token) SetCookie(w http.ResponseWriter)
- func (t *Token) Signature() string
- func (t *Token) String() string
Examples ¶
Constants ¶
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 = 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 ¶
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.
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 ¶
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 ¶
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) Parse ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
Source Files
¶
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. |