Commit40113aa8Recorded1 Jan 2026Repositorysigil-jwt

Add sigil-jwt package for JSON Web Tokens

Message

Provides JWT creation and verification using HS256: - jwt-encode: Create signed tokens from claims alist - jwt-decode: Verify signature and return claims (or #f) - jwt-decode-unsafe: Decode without verification (for inspection) - Automatic iat (issued-at) claim - Expiration checking via exp claim

Changed
 package.sgl       |  32 +++++++++++++++++++++++++++++++
 src/sigil/jwt.sgl | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 214 insertions(+)
Diff
package.sgladded
@@ -0,0 +1,32 @@
+1
;;; sigil-jwt - JSON Web Token Library
+2
;;;
+3
;;; Create and verify JWT tokens for authentication.
+4
;;;
+5
;;; API:
+6
;;; (jwt-encode claims secret) - Create signed token
+7
;;; (jwt-decode token secret) - Verify and decode token
+8
;;; (jwt-decode-unsafe token) - Decode without verification
+9
+10
(package
+11
name: "sigil-jwt"
+12
version: "0.0.0"
+13
description: "JSON Web Token library for Sigil"
+14
license: "MIT"
+15
+16
dependencies: (list
+17
(package-dep name: "sigil-stdlib" version: ">= 0.0.0")
+18
(package-dep name: "sigil-json" version: ">= 0.0.0")
+19
(package-dep name: "sigil-time" version: ">= 0.0.0"))
+20
+21
source-dirs: '("src/sigil")
+22
+23
tasks: (list
+24
(task
+25
name: 'build
+26
description: "Compile Scheme modules"
+27
steps: (list
+28
(ensure-dirs dirs: '("lib/sigil"))
+29
+30
(compile-sigil-module
+31
source: "src/sigil/jwt.sgl"
+32
output: (config-output-subdir "lib/sigil/jwt.sgb"))))))
src/sigil/jwt.sgladded
@@ -0,0 +1,182 @@
+1
;;; (sigil jwt) - JSON Web Token Library
+2
;;;
+3
;;; Create and verify JWT tokens for authentication.
+4
;;;
+5
;;; ## Creating tokens
+6
;;;
+7
;;; ```scheme
+8
;;; (import (sigil jwt))
+9
;;;
+10
;;; (define token (jwt-encode '((sub . "user123")
+11
;;; (email . "[email protected]"))
+12
;;; "my-secret-key"))
+13
;;; ```
+14
;;;
+15
;;; ## Verifying tokens
+16
;;;
+17
;;; ```scheme
+18
;;; (let ((claims (jwt-decode token "my-secret-key")))
+19
;;; (if claims
+20
;;; (display (assoc-ref 'sub claims))
+21
;;; (display "Invalid token")))
+22
;;; ```
+23
+24
(define-library (sigil jwt)
+25
(import (sigil core)
+26
(sigil crypto)
+27
(sigil json)
+28
(sigil time)
+29
(sigil string))
+30
+31
(export
+32
jwt-encode
+33
jwt-decode
+34
jwt-decode-unsafe)
+35
+36
(begin
+37
+38
;; ============================================================
+39
;; Base64URL Encoding
+40
;; ============================================================
+41
;;
+42
;; JWT uses base64url encoding which differs from standard base64:
+43
;; - '+' is replaced with '-'
+44
;; - '/' is replaced with '_'
+45
;; - Trailing '=' padding is removed
+46
+47
;; Works with both strings and bytevectors
+48
(define (base64url-encode data)
+49
(let ((b64 (base64-encode data)))
+50
(chain b64
+51
(string-replace _ "+" "-")
+52
(string-replace _ "/" "_")
+53
(string-trim-end _ "="))))
+54
+55
(define (base64url-decode str)
+56
;; Add back padding if needed
+57
(let* ((len (string-length str))
+58
(padding (case (modulo len 4)
+59
((2) "==")
+60
((3) "=")
+61
(else "")))
+62
(padded (string-append str padding))
+63
;; Convert back to standard base64
+64
(b64 (chain padded
+65
(string-replace _ "-" "+")
+66
(string-replace _ "_" "/"))))
+67
(let ((decoded (base64-decode b64)))
+68
(if decoded
+69
(utf8->string decoded)
+70
#f))))
+71
+72
;; ============================================================
+73
;; JWT Implementation
+74
;; ============================================================
+75
+76
;;; Create a signed JWT token from claims.
+77
;;;
+78
;;; Claims should be an alist of claim names to values.
+79
;;; Common claims:
+80
;;; sub - Subject (user ID)
+81
;;; iat - Issued at (Unix timestamp, added automatically)
+82
;;; exp - Expiration (Unix timestamp)
+83
;;; email, username, etc.
+84
;;;
+85
;;; Returns a JWT string in the format: header.payload.signature
+86
;;;
+87
;;; Examples:
+88
;;; ```scheme
+89
;;; (jwt-encode '((sub . "12345") (email . "[email protected]"))
+90
;;; "secret-key")
+91
;;; ; => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+92
;;; ```
+93
(define (jwt-encode claims secret)
+94
(let* (;; Header is always the same for HS256
+95
(header-json "{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
+96
(header-b64 (base64url-encode header-json))
+97
;; Add iat (issued at) if not present
+98
(claims-with-iat
+99
(if (assq 'iat claims)
+100
claims
+101
(cons (cons 'iat (floor (current-second))) claims)))
+102
;; Encode claims as JSON
+103
(payload-json (json-encode claims-with-iat))
+104
(payload-b64 (base64url-encode payload-json))
+105
;; Create signature
+106
(signing-input (string-append header-b64 "." payload-b64))
+107
(signature-hex (hmac-sha256 secret signing-input))
+108
(signature-bytes (hex->bytevector signature-hex))
+109
(signature-b64 (base64url-encode signature-bytes)))
+110
(string-append header-b64 "." payload-b64 "." signature-b64)))
+111
+112
;;; Decode and verify a JWT token.
+113
;;;
+114
;;; Returns the claims alist if the signature is valid, or #f if invalid.
+115
;;; This also checks the exp claim if present and rejects expired tokens.
+116
;;;
+117
;;; Examples:
+118
;;; ```scheme
+119
;;; (jwt-decode "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." "secret-key")
+120
;;; ; => ((sub . "12345") (email . "[email protected]") (iat . 1234567890))
+121
;;;
+122
;;; (jwt-decode "invalid.token.here" "secret-key")
+123
;;; ; => #f
+124
;;; ```
+125
(define (jwt-decode token secret)
+126
(let ((parts (string-split token ".")))
+127
(if (not (= (length parts) 3))
+128
#f ;; Invalid format
+129
(let* ((header-b64 (car parts))
+130
(payload-b64 (cadr parts))
+131
(signature-b64 (caddr parts))
+132
;; Verify signature
+133
(signing-input (string-append header-b64 "." payload-b64))
+134
(expected-hex (hmac-sha256 secret signing-input))
+135
(expected-bytes (hex->bytevector expected-hex))
+136
(expected-sig (base64url-encode expected-bytes)))
+137
(if (not (string=? signature-b64 expected-sig))
+138
#f ;; Invalid signature
+139
;; Decode payload
+140
(let* ((payload-json (base64url-decode payload-b64))
+141
(claims (and payload-json (json-decode payload-json))))
+142
(if (not claims)
+143
#f ;; Invalid payload
+144
;; Check expiration if present
+145
(let ((exp (dict-ref claims exp: #f)))
+146
(if (and exp (< exp (floor (current-second))))
+147
#f ;; Token expired
+148
claims)))))))))
+149
+150
;;; Decode a JWT without verifying the signature.
+151
;;;
+152
;;; Use this only when you need to inspect token claims before
+153
;;; verification (e.g., to determine which secret to use).
+154
;;; NEVER trust these claims for authentication.
+155
;;;
+156
;;; Returns the claims alist, or #f if the token is malformed.
+157
(define (jwt-decode-unsafe token)
+158
(let ((parts (string-split token ".")))
+159
(if (not (= (length parts) 3))
+160
#f
+161
(let* ((payload-b64 (cadr parts))
+162
(payload-json (base64url-decode payload-b64)))
+163
(and payload-json (json-decode payload-json))))))
+164
+165
;; ============================================================
+166
;; Helpers
+167
;; ============================================================
+168
+169
;; Convert hex string to bytevector
+170
(define (hex->bytevector hex)
+171
(let* ((len (/ (string-length hex) 2))
+172
(bv (make-bytevector len)))
+173
(let loop ((i 0))
+174
(if (>= i len)
+175
bv
+176
(let ((byte (string->number
+177
(substring hex (* i 2) (+ (* i 2) 2))
+178
16)))
+179
(bytevector-u8-set! bv i byte)
+180
(loop (+ i 1)))))))
+181
+182
))