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
))