jwt

examples
Login

examples

SQLITE_NOTICE(539): recovered 2 pages from /srv/fossil/jwt.fossil-journal

Examples

All examples on this page were run against the package as they stand here.


Signing a token and verifying it again:

package require jwt

set header  {{"alg":"HS256","typ":"JWT"}}
set payload {{"sub":"alice","name":"Alice Müller"}}
set secret  0123456789abcdef0123456789abcdef

set token [::jwt::sign $header $payload $secret]

::jwt::verify $token $secret
# -> true

The header must carry alg, there is no default, and the secret has a lower bound: 32 bytes for HS256, 48 for HS384, 64 for HS512.


Building a payload with an expiry time. The claims are JSON text, so either substitute the value or let rl_json build it:

set exp [expr {[clock seconds] + 3600}]

# plain Tcl
set payload [subst {{"sub":"alice","exp":$exp}}]

# or with rl_json, which quotes and types the value for you
package require rl_json
set payload [rl_json::json template {{"sub":"alice","exp":"~N:exp"}}]

set token [::jwt::sign $header $payload $secret]

Checking the expiry. Verification is signature-only unless -claims is given, so an expired token verifies as true without it. That is deliberate and it is the one place where the decision is left to the caller:

set exp [expr {[clock seconds] - 60}]
set old [::jwt::sign $header [subst {{"sub":"alice","exp":$exp}}] $secret]

::jwt::verify $old $secret
# -> true            signature is fine, exp was not looked at

::jwt::verify $old $secret -claims
# -> false           expired one minute ago

::jwt::verify $old $secret -claims -leeway 120
# -> true            within the tolerance for clock skew

-leeway takes effect together with -claims only. An invalid value is an error rather than a silent zero, so a typo cannot switch the check off.


Finding out why a token was rejected. With -json the result is an rl_json document; together with -claims it carries reason:

set res [::jwt::verify $old $secret -claims -json]

rl_json::json get $res verify
# -> 0
rl_json::json get $res reason
# -> expired

set claims [rl_json::json get $res payload]
rl_json::json get $claims sub
# -> alice

reason is 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. header and payload hold raw, re-parsable JSON text, which is why reading a claim takes the two steps shown above.


Restricting the algorithm. A token names its own algorithm in the header, so a verifier that accepts whatever it finds there lets the sender choose. Name what you accept:

set key [string repeat k 64]
set token [::jwt::sign {{"alg":"HS512","typ":"JWT"}} {{"sub":"alice"}} $key]

::jwt::verify $token $key -alg HS256
# -> false           the header asks for HS512, which was not accepted

::jwt::verify $token $key -alg HS512
# -> true

::jwt::verify $token $key -alg {HS256 HS512}
# -> true            a list works as well

Verifying a token from an OpenID Connect provider. The client secret is chosen by the other side and used at whatever length it has - 64 characters is what the Nextcloud app OIDC Identity Provider generates:

set clientSecret $secretFromTheProvider

set res [::jwt::verify $idToken $clientSecret -claims -alg HS256 -json]

if {[rl_json::json get $res verify]} {
  set claims [rl_json::json get $res payload]
  puts "issuer  [rl_json::json get $claims iss]"
  puts "subject [rl_json::json get $claims sub]"
} else {
  puts "rejected: [rl_json::json get $res reason]"
}

Do not pad, hash or shorten such a secret beforehand - any of that produces a different tag. It goes in unchanged, the RFC 2104 key preparation happens inside nacl.


HS384 and HS512 work the same way, only the header and the minimum key length differ:

::jwt::sign {{"alg":"HS384","typ":"JWT"}} $payload [string repeat k 48]
::jwt::sign {{"alg":"HS512","typ":"JWT"}} $payload [string repeat k 64]

Errors versus rejected tokens. A bad token makes verify return false, a mistake on your side raises:

catch {::jwt::sign {{"typ":"JWT"}} $payload $secret} err
# jwt::sign: header has no alg (RFC 7515 section 4.1.1 requires it)

catch {::jwt::sign $header $payload kurz} err
# jwt::sign: HS256 requires a key of at least 32 bytes (RFC 7518 section 3.2), got 4

::jwt::verify garbage.not.atoken $secret
# -> false           malformed input is rejected, not raised

Generating a secret of the right size, and putting it somewhere text-safe:

package require nacl

set key [nacl::randombytes 32]
set text [::jwt::base64url_encode $key]
# 43 characters, no padding, safe in a URL or a config file

set key [::jwt::base64url_decode $text]

base64url_encode and base64url_decode are public for exactly this purpose - keys, salts and binary blobs. They do no character conversion, so they are safe for arbitrary bytes.