-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathjwt.xqm
154 lines (134 loc) · 4.86 KB
/
jwt.xqm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
xquery version "3.1";
module namespace jwt = "http://existsolutions.com/ns/jwt";
import module namespace crypto ="http://expath.org/ns/crypto";
declare variable $jwt:epoch-start := xs:dateTime("1970-01-01T00:00:00Z");
declare variable $jwt:default-token-lifetime := 30*24*60*60; (:xs:dayTimeDuration("P30D");:)
declare variable $jwt:header := jwt:encode(map { "alg": "HS256", "typ": "JWT" });
(:~
: Returns a map with two keys: "create" and "read".
: Both are partially applied functions with an arity of one.
: This is for comfort, having to pass only the payload to "create" and
: the token to "read".
:
: @param $secret a longer string which will be used to sign tokens
: @param $lifetime the number of seconds each issued token stays valid
: @returns map(xs:string, function(*))
:)
declare function jwt:instance ($secret as xs:string, $lifetime as xs:integer) as map(*) {
let $now := current-dateTime() => jwt:dateTime-to-epoch()
return
map {
"create" : jwt:create(?, $now, $secret),
"read" : jwt:read(?, $secret, $lifetime)
}
};
(:~
: Issue a signed JWT
:
: @param $payload any map(*) - the key "iat", for issued at, will be added
: @param $time seconds since $jwt:epoch-start, will be the value for "iat"
: @param $secret the signing key
: @return xs:string the signed token
:)
declare function jwt:create ($payload as map(*), $time as xs:integer, $secret as xs:string) as xs:string {
let $enc-payload :=
$payload
=> map:put("iat", $time)
=> jwt:encode()
return
(
$jwt:header,
$enc-payload,
jwt:sign($jwt:header || "." || $enc-payload, $secret)
)
=> string-join(".")
};
(:~
: Issue a signed JWT
:
: @param $token a JWT to read and verify
: @param $secret the signing key
: @param $lifetime how old, in seconds, the token is allowed to be
: @return xs:string the signed token
:)
declare function jwt:read ($token as xs:string, $secret as xs:string, $lifetime as xs:integer) as item()? {
let $parts := tokenize($token, "\.")
return
if (count($parts) ne 3)
then (error(xs:QName("invalid-token")))
else if ($parts[1] ne $jwt:header)
then (error(xs:QName("invalid-header")))
else if (jwt:verify-signature($parts[2], $parts[3], $secret))
then (
(: verify token lifetime (iat) :)
let $payload := jwt:decode($parts[2])
let $dt := jwt:dateTime-to-epoch(current-dateTime()) - $payload?iat
return
if ($dt > $lifetime)
then (error(xs:QName("too-old"), $dt, jwt:epoch-to-dateTime($payload?iat)))
else if ($dt < 0)
then (error(xs:QName("future-date"), $dt, jwt:epoch-to-dateTime($payload?iat)))
else ($payload)
)
else (error("invalid-signature"))
};
declare function jwt:sign ($data as xs:string, $secret as xs:string) as xs:string {
crypto:hmac($data, $secret, "HMAC-SHA-256", "base64")
=> jwt:base64-url-safe()
};
(:~
: verify signature
:)
declare function jwt:verify-signature ($payload as xs:string, $signature as xs:string, $secret as xs:string) as xs:boolean {
jwt:sign($jwt:header || "." || $payload, $secret) eq $signature
};
declare function jwt:read-header ($header-value as xs:string, $secret as xs:string, $lifetime as xs:integer) as item()? {
substring-after($header-value, "Bearer ")
=> jwt:read($secret, $lifetime)
};
declare function jwt:dateTime-to-epoch($dateTime as xs:dateTime) as xs:integer {
($dateTime - $jwt:epoch-start) div xs:dayTimeDuration('PT1S')
};
declare function jwt:epoch-to-dateTime($ts as xs:integer) as xs:dateTime {
$jwt:epoch-start + xs:dayTimeDuration(concat("PT", $ts, "S"))
};
declare
function jwt:encode ($data as item()) as xs:string {
$data
=> serialize(map { "method": "json" })
=> util:base64-encode(true())
=> jwt:base64-url-safe()
};
declare
function jwt:decode ($base64 as xs:string) as item()? {
$base64
(: base64-decode might to be able to handle url-safe encoded data :)
=> translate('-_', '/+')
=> jwt:base64-pad()
=> util:base64-decode()
=> parse-json()
};
(:~
: add padding (= or ==) otherwise util:base64-decode() throws an error
:)
declare %private
function jwt:base64-pad ($data as xs:string) as xs:string {
let $mod4 := string-length($data) mod 4
let $pad :=
switch ($mod4)
case 2 return "=="
case 3 return "="
default return ""
return
$data || $pad
};
(:~
: convert base64 string to url-safe without padding
: replace / and + with - and _
: omit padding (=)
: @see https://tools.ietf.org/html/rfc4648
:)
declare %private
function jwt:base64-url-safe ($base64 as xs:string) as xs:string {
$base64 => translate('+/=', '-_')
};