jwt - JSON Web Tokens
Synopsis
Sign and verify JSON Web Tokens in the JWS Compact Serialization, the
header.payload.signature form that RFC 7519 and RFC 7515 define.
The package (package jwt) is pure Tcl and has two main commands. Create a token with ::jwt::sign, check one with ::jwt::verify:
package require jwt
set header {{"alg":"HS256","typ":"JWT"}}
set payload {{"sub":"alice","exp":1893456000}}
set secret 0123456789abcdef0123456789abcdef
set token [::jwt::sign $header $payload $secret]
::jwt::verify $token $secret ;# signature only
::jwt::verify $token $secret -claims ;# also exp and nbf
::jwt::verify $token $secret -claims -leeway 60 ;# with clock skew
::jwt::verify $token $secret -alg HS256 ;# restrict the algorithm
Without options ::jwt::verify returns a boolean and checks the signature
only, which means an expired token verifies as true. The time claims exp and
nbf are evaluated with -claims alone, -leeway adds a tolerance in
seconds against clock skew and works together with -claims only.
-alg names the algorithms the caller accepts, for instance -alg HS256 or
-alg {HS256 HS512}. A token whose header asks for anything else then fails,
no matter what it claims. Without the option every implemented algorithm is
accepted.
With -json the result is an rl_json document holding verify, header and
payload, the latter two as raw re-parsable JSON text. Together with
-claims it also carries reason, one of ok, malformed, alg, crit,
signature, notbefore, expired or payload, so a forged token can be told
apart from one that merely ran out of time:
set res [::jwt::verify $token $secret -claims -json]
rl_json::json get $res verify ;# 1 or 0
rl_json::json get $res reason ;# ok, expired, ...
set claims [rl_json::json get $res payload]
rl_json::json get $claims sub ;# alice
::jwt::base64url_encode and ::jwt::base64url_decode are public as well, because callers need base64url for salts, keys and binary blobs of their own.
For working code covering all of this, see the examples at this site.
Algorithms
The algorithm is taken from the alg field of the header, which RFC 7515
section 4.1.1 makes mandatory - there is no default.
| alg | MAC | Tag | Minimum key |
|---|---|---|---|
| HS256 | HMAC-SHA-256 | 32 bytes | 32 bytes |
| HS384 | HMAC-SHA-384 | 48 bytes | 48 bytes |
| HS512 | HMAC-SHA-512 | 64 bytes | 64 bytes |
| NaCl | Poly1305 onetime authentication | 16 bytes | 32 bytes, fixed |
The three HS* algorithms are RFC 7518 section 3.2 as written. The secret is
used at whatever length it has, prepared as RFC 2104 section 2 requires, so a
client secret goes in unchanged - no padding and no hashing beforehand, either
would produce a different tag. The lower bound in the table is the one RFC 7518
section 3.2 sets, namely the size of the hash output.
NaCl is not a registered JWS algorithm and interoperates with this package
only. It has a fixed 32 byte key, which is why a secret for it, and only for
it, is NUL-padded or truncated.
Dependencies
- Tcl >= 8.6, Tcl 9 included. The package is pure Tcl, so one installed copy serves both.
- nacl >= 1.4 for the HMAC primitives. The untruncated
-hmac384and-hmac512that HS384 and HS512 are defined on arrived in 1.4; on older versions Tcl's prefix matching of option names turns-hmac512into-hmac512256, which succeeds and returns half a tag with no error to catch. - rl_json for JSON access.
Both are C extensions and are loaded lazily on the first sign or verify
call, so sourcing the package costs nothing while it stays unused.
Conformance
Implemented as the specifications require, each covered by a regression test:
the mandatory alg including the rejection of the unsecured none, the crit
header, canonical base64url without padding, UTF-8 encoding of header and
payload, both of them JSON objects, the minimum key size, and -alg so the
caller decides which algorithms are acceptable.
One deviation is deliberate: validation of the time claims is opt-in. Without -claims an expired token verifies as true, which keeps the signature-only contract existing callers rely on.
Characters beyond the BMP, above U+FFFF, cannot be converted to UTF-8 by Tcl 8.6 - it substitutes U+FFFD and the character is lost. Tcl 9 encodes them correctly. Everything within the BMP, umlauts and CJK included, is identical on both.
Wiki Pages
See Also
- RFC 7519 JSON Web Token
- RFC 7515 JSON Web Signature
- RFC 7518 JSON Web Algorithms
- RFC 8725 JSON Web Token Best Current Practices
- RFC 2104 HMAC
- nacl
- rl_json
- websh - this package was part of websh up to version 3.7.7
Legal Notice
Copyright (C) 2022-2026 Alexander Schoepe, Bochum, DE
Tcl package: BSD-3 license