Class ::ns_crypto::JWT (public)
::nx::Class ::ns_crypto::JWT![[i]](/resources/acs-subsite/ZoomIn16.gif)
Defined in
- Testcases:
-
No testcase defined.
Source code:
namespace eval ::ns_crypto {}
::nsf::object::alloc ::nx::Class ::ns_crypto::JWT {}
::ns_crypto::JWT protected method alg_to_hmac_digest alg {
switch -- $alg {
HS256 { return sha256 }
HS384 { return sha384 }
HS512 { return sha512 }
default {
error "unsupported JWT HMAC algorithm \"$alg\""
}
}
}
::ns_crypto::JWT protected method alg_to_verify_spec alg {
switch -- $alg {
ES256 {return {digest sha256}}
ES256K {return {digest sha256}}
ES384 {return {digest sha384}}
ES512 {return {digest sha512}}
RS256 {return {digest sha256}}
RS384 {return {digest sha384}}
RS512 {return {digest sha512}}
EdDSA {return {}}
default {
error "unsupported JWT algorithm \"$alg\""
}
}
}
::ns_crypto::JWT protected method build_protected_header {-alg:required {-kid ""} {-typ "JWT"} {-cty ""} {-extraheader ""}} {
set triples [list alg string $alg]
if {$typ ne ""} {
lappend triples typ string $typ
}
if {$cty ne ""} {
lappend triples cty string $cty
}
if {$kid ne ""} {
lappend triples kid string $kid
}
if {$extraheader ne ""} {
foreach {k t v} $extraheader {
lappend triples $k $t $v
}
}
set json [ns_json value -type object $triples]
return $json
}
::ns_crypto::JWT public method decode token {
lassign [:split_token $token] headerB64 payloadB64 sigB64
set headerJson [ns_base64urldecode -- $headerB64]
set payloadJson [ns_base64urldecode -- $payloadB64]
set header [ns_json parse $headerJson]
set payload [ns_json parse $payloadJson]
return [dict create header $header payload $payload signature [ns_base64urldecode -binary -- $sigB64]]
}
::ns_crypto::JWT public method encode {-alg:required {-key ""} {-jwk ""} {-secret ""} {-kid ""} {-typ "JWT"} {-cty ""} {-extraheader ""} -iss -sub -aud -exp -nbf -iat -jti {extrapayload ""}} {
set headerJson [:build_protected_header -alg $alg -kid $kid -typ $typ -cty $cty -extraheader $extraheader]
set triples {}
foreach field {iss sub aud exp nbf iat jti} {
if {![info exists $field]} continue
switch $field {
exp -
nbf -
iat { lappend triples $field number [set $field] }
aud {
if {[llength $aud] == 1} {
lappend triples aud string $aud
} else {
set array [lmap a $aud {list 1 string $a}]
lappend triples aud array [concat {*}$array]
}
}
default { lappend triples $field string [set $field] }
}
}
lappend triples {*}$extrapayload
set payloadJson [ns_json value -type object $triples]
set headerB64 [ns_base64urlencode -- $headerJson]
set payloadB64 [ns_base64urlencode -- $payloadJson]
set signingInput "${headerB64}.${payloadB64}"
if {$alg in {HS256 HS384 HS512}} {
if {$secret eq ""} {
error "missing shared secret; provide -secret"
}
set signature [:hmac_sign -alg $alg -secret $secret -data $signingInput]
} elseif {$alg eq "none"} {
return "${signingInput}."
} else {
set pem [:resolve_signing_key_pem -alg $alg -key $key -jwk $jwk]
set signature [:sign -alg $alg -pem $pem -data $signingInput]
}
set sigB64 [ns_base64urlencode -binary -- $signature]
return "${signingInput}.${sigB64}"
}
::ns_crypto::JWT protected method hmac_sign {{-alg ""} {-secret ""} {-data ""}} {
set digest [:alg_to_hmac_digest $alg]
return [ns_crypto::hmac string -digest $digest -encoding binary -- $secret $data]
}
::ns_crypto::JWT protected method jwk_ec_curve_to_ns_curve crv {
switch -- $crv {
P-256 {return prime256v1}
secp256k1 {return secp256k1}
P-384 {return secp384r1}
P-521 {return secp521r1}
default {
error "unsupported EC JWK curve \"$crv\""
}
}
}
::ns_crypto::JWT protected method jwk_supports_alg {jwk alg} {
if {[dict exists $jwk alg]} {
return [string equal [dict get $jwk alg] $alg]
}
dict with jwk {
switch -- $alg {
ES256 {return [expr {$kty eq "EC" && $crv eq "P-256" }]}
ES256K {return [expr {$kty eq "EC" && $crv eq "secp256k1" }]}
ES384 {return [expr {$kty eq "EC" && $crv eq "P-384" }]}
ES512 {return [expr {$kty eq "EC" && $crv eq "P-521" }]}
RS256 -
RS384 -
RS512 {return [expr {$kty eq "RSA"}]}
EdDSA {return [expr {$kty eq "OKP" && $crv in {"Ed25519" "Ed448"}}]}
default {
return 0
}
}
}
}
::ns_crypto::JWT protected method public_key_from_jwk jwk {
set kty [dict get $jwk kty]
switch -- $kty {
EC {
set crv [dict get $jwk crv]
set x [ns_base64urldecode -binary -- [dict get $jwk x]]
set y [ns_base64urldecode -binary -- [dict get $jwk y]]
set nsCurve [:jwk_ec_curve_to_ns_curve $crv]
return [ns_crypto::key import -from public -name EC -params [list group $nsCurve x $x y $y] -format pem]
}
RSA {
set n [ns_base64urldecode -binary -- [dict get $jwk n]]
set e [ns_base64urldecode -binary -- [dict get $jwk e]]
return [ns_crypto::key import -from public -name RSA -params [list n $n e $e] -format pem]
}
OKP {
set crv [dict get $jwk crv]
set x [ns_base64urldecode -binary -- [dict get $jwk x]]
return [ns_crypto::key import -from public -name OKP -params [list crv $crv x $x] -format pem]
}
default {
error "unsupported JWK key type \"$kty\""
}
}
}
::ns_crypto::JWT protected method resolve_signing_key_pem {{-alg ""} {-key ""} {-jwk ""}} {
if {$key ne ""} {
return $key
}
if {$jwk ne ""} {
error "encode with -jwk is not implemented yet; provide -key PEM"
}
error "missing signing key; provide -key"
}
::ns_crypto::JWT protected method resolve_verification_key_pem {{-alg ""} {-key ""} {-jwk ""} {-jwks ""} {-kid ""}} {
if {$key ne ""} {
return $key
}
if {$jwk ne ""} {
return [:public_key_from_jwk $jwk]
}
if {$jwks ne ""} {
set jwk [:select_jwk_from_jwks -jwks $jwks -alg $alg -kid $kid]
return [:public_key_from_jwk $jwk]
}
error "missing verification key; provide -key, -jwk, or -jwks"
}
::ns_crypto::JWT protected method select_jwk_from_jwks {{-jwks ""} {-alg ""} {-kid ""}} {
if {![dict exists $jwks keys]} {
error "invalid JWKS: missing \"keys\""
}
set matches {}
foreach jwk [dict get $jwks keys] {
if {$kid ne "" && [dict exists $jwk kid] && [dict get $jwk kid] ne $kid} {
continue
}
if {$kid ne "" && ![dict exists $jwk kid]} {
continue
}
if {[:jwk_supports_alg $jwk $alg]} {
lappend matches $jwk
}
}
if {[llength $matches] == 0} {
error "no matching JWK found in JWKS"
}
if {[llength $matches] > 1} {
error "JWKS lookup is ambiguous; multiple keys match"
}
return [lindex $matches 0]
}
::ns_crypto::JWT protected method sign {{-alg ""} {-pem ""} {-data ""}} {
switch -- $alg {
ES256 {return [ns_crypto::signature sign -digest sha256 -encoding binary -pem $pem -- $data]}
ES256K {return [ns_crypto::signature sign -digest sha256 -encoding binary -pem $pem -- $data]}
ES384 {return [ns_crypto::signature sign -digest sha384 -encoding binary -pem $pem -- $data]}
ES512 {return [ns_crypto::signature sign -digest sha512 -encoding binary -pem $pem -- $data]}
RS256 {return [ns_crypto::signature sign -digest sha256 -encoding binary -pem $pem -- $data]}
RS384 {return [ns_crypto::signature sign -digest sha384 -encoding binary -pem $pem -- $data]}
RS512 {return [ns_crypto::signature sign -digest sha512 -encoding binary -pem $pem -- $data]}
EdDSA {return [ns_crypto::signature sign -encoding binary -pem $pem -- $data]}
default {
error "unsupported JWT algorithm \"$alg\""
}
}
}
::ns_crypto::JWT protected method split_token token {
set parts [split $token .]
if {[llength $parts] != 3} {
error "invalid JWT: expected 3 dot-separated components"
}
return $parts
}
::ns_crypto::JWT public method verify {{-alg ""} {-key ""} {-jwk ""} {-jwks ""} {-secret ""} {-kid ""} -requirekid:switch -verifyclaims:switch {-aud ""} {-iss ""} {-sub ""} {-clockskew 0} {-now ""} token} {
lassign [:split_token $token] headerB64 payloadB64 sigB64
set headerJson [ns_base64urldecode -- $headerB64]
set payloadJson [ns_base64urldecode -- $payloadB64]
set signature [ns_base64urldecode -binary -- $sigB64]
set header [ns_json parse $headerJson]
set payload [ns_json parse $payloadJson]
set tokenAlg [dict get $header alg]
if {$alg ne "" && $alg ne $tokenAlg} {
error "JWT algorithm mismatch: expected \"$alg\", got \"$tokenAlg\""
}
if {$tokenAlg eq "none"} {
error "unsigned JWTs (alg=none) are not accepted by verify"
}
set signingInput "${headerB64}.${payloadB64}"
set resolvedKid [expr {[dict exists $header kid] ? [dict get $header kid] : ""}]
if {$requirekid && $resolvedKid eq ""} {
error "JWT header does not contain required kid"
}
if {$kid ne "" && $resolvedKid ne $kid} {
error "JWT kid mismatch: expected \"$kid\", got \"$resolvedKid\""
}
if {$tokenAlg in {HS256 HS384 HS512}} {
if {$secret eq ""} {
error "missing shared secret; provide -secret"
}
if {![:verify_hmac -alg $tokenAlg -secret $secret -data $signingInput -signature $signature]} {
error "JWT signature verification failed"
}
} else {
set verifyPem [:resolve_verification_key_pem -alg $tokenAlg -key $key -jwk $jwk -jwks $jwks -kid $resolvedKid]
set verifySpec [:alg_to_verify_spec $tokenAlg]
if {![:verify_signature -spec $verifySpec -pem $verifyPem -data $signingInput -signature $signature]} {
error "JWT signature verification failed"
}
}
if {$verifyclaims} {
:verify_registered_claims -payload $payload -aud $aud -iss $iss -sub $sub -clockskew $clockskew -now $now
}
return [dict create valid 1 header $header payload $payload kid $resolvedKid alg $tokenAlg]
}
::ns_crypto::JWT protected method verify_hmac {{-alg ""} {-secret ""} {-data ""} {-signature ""}} {
set expected [:hmac_sign -alg $alg -secret $secret -data $data]
return [string equal $expected $signature]
}
::ns_crypto::JWT protected method verify_registered_claims {{-payload ""} {-aud ""} {-iss ""} {-sub ""} {-clockskew 0} {-now ""}} {
if {$now eq ""} {
set now [clock seconds]
}
if {[dict exists $payload exp]} {
set exp [dict get $payload exp]
if {$now > ($exp + $clockskew)} {
error "JWT expired"
}
}
if {[dict exists $payload nbf]} {
set nbf [dict get $payload nbf]
if {$now < ($nbf - $clockskew)} {
error "JWT not valid yet"
}
}
if {[dict exists $payload iat]} {
set iat [dict get $payload iat]
if {$iat > ($now + $clockskew)} {
error "JWT issued-at time is in the future"
}
}
if {$iss ne ""} {
if {![dict exists $payload iss] || [dict get $payload iss] ne $iss} {
error "JWT issuer mismatch"
}
}
if {$sub ne ""} {
if {![dict exists $payload sub] || [dict get $payload sub] ne $sub} {
error "JWT subject mismatch"
}
}
if {$aud ne ""} {
if {![dict exists $payload aud]} {
error "JWT audience missing"
}
set tokenAud [dict get $payload aud]
if {[llength $tokenAud] > 1} {
if {$aud ni $tokenAud} {
error "JWT audience mismatch"
}
} else {
if {$tokenAud ne $aud} {
error "JWT audience mismatch"
}
}
}
}
::ns_crypto::JWT protected method verify_signature {{-spec ""} {-pem ""} {-data ""} {-signature ""}} {
set cmd [list ns_crypto::signature verify]
foreach {k v} $spec {
lappend cmd -$k $v
}
lappend cmd -pem $pem -signature $signature -- $data
return [uplevel 1 $cmd]
}
XQL Not present:Generic, PostgreSQL, Oracle
[
hide source ]
| [
make this the default ]