forked from jitsi/luajwtjitsi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
luajwtjitsi.lua
259 lines (217 loc) · 8.08 KB
/
luajwtjitsi.lua
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
local cjson_safe = require 'cjson.safe'
local basexx = require 'basexx'
local digest = require 'openssl.digest'
local hmac = require 'openssl.hmac'
local pkey = require 'openssl.pkey'
-- Generates an RSA signature of the data.
-- @param data The data to be signed.
-- @param key The private signing key in PEM format.
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
-- @return The signature or nil and an error message.
local function signRS (data, key, algo)
local privkey = pkey.new(key)
if privkey == nil then
return nil, 'Not a private PEM key'
else
local datadigest = digest.new(algo):update(data)
return privkey:sign(datadigest)
end
end
-- Verifies an RSA signature on the data.
-- @param data The signed data.
-- @param signature The signature to be verified.
-- @param key The public key of the signer.
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
-- @return True if the signature is valid, false otherwise. Also returns false if the key is invalid.
local function verifyRS (data, signature, key, algo)
local pubkey = pkey.new(key)
if pubkey == nil then
return false
end
local datadigest = digest.new(algo):update(data)
return pubkey:verify(signature, datadigest)
end
local alg_sign = {
['HS256'] = function(data, key) return hmac.new(key, 'sha256'):final(data) end,
['HS384'] = function(data, key) return hmac.new(key, 'sha384'):final(data) end,
['HS512'] = function(data, key) return hmac.new(key, 'sha512'):final(data) end,
['RS256'] = function(data, key) return signRS(data, key, 'sha256') end,
['RS384'] = function(data, key) return signRS(data, key, 'sha384') end,
['RS512'] = function(data, key) return signRS(data, key, 'sha512') end
}
local alg_verify = {
['HS256'] = function(data, signature, key) return signature == alg_sign['HS256'](data, key) end,
['HS384'] = function(data, signature, key) return signature == alg_sign['HS384'](data, key) end,
['HS512'] = function(data, signature, key) return signature == alg_sign['HS512'](data, key) end,
['RS256'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha256') end,
['RS384'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha384') end,
['RS512'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha512') end
}
-- Splits a token into segments, separated by '.'.
-- @param token The full token to be split.
-- @return A table of segments.
local function split_token(token)
local segments={}
for str in string.gmatch(token, "([^\\.]+)") do
table.insert(segments, str)
end
return segments
end
-- Parses a JWT token into it's header, body, and signature.
-- @param token The JWT token to be parsed.
-- @return A JSON header and body represented as a table, and a signature.
local function parse_token(token)
local segments=split_token(token)
if #segments ~= 3 then
return nil, nil, nil, "Invalid token"
end
local header, err = cjson_safe.decode(basexx.from_url64(segments[1]))
if err then
return nil, nil, nil, "Invalid header"
end
local body, err = cjson_safe.decode(basexx.from_url64(segments[2]))
if err then
return nil, nil, nil, "Invalid body"
end
local sig, err = basexx.from_url64(segments[3])
if err then
return nil, nil, nil, "Invalid signature"
end
return header, body, sig
end
-- Removes the signature from a JWT token.
-- @param token A JWT token.
-- @return The token without its signature.
local function strip_signature(token)
local segments=split_token(token)
if #segments ~= 3 then
return nil, nil, nil, "Invalid token"
end
table.remove(segments)
return table.concat(segments, ".")
end
-- Verifies that a claim is in a list of allowed claims. Allowed claims can be exact values, or the
-- catch all wildcard '*'.
-- @param claim The claim to be verified.
-- @param acceptedClaims A table of accepted claims.
-- @return True if the claim was allowed, false otherwise.
local function verify_claim(claim, acceptedClaims)
for i, accepted in ipairs(acceptedClaims) do
if accepted == '*' then
return true;
end
if claim == accepted then
return true;
end
end
return false;
end
local M = {}
-- Encodes the data into a signed JWT token.
-- @param data The data the put in the body of the JWT token.
-- @param key The key to use for signing the JWT token.
-- @param alg The signature algorithm to use: HS256, HS384, HS512, RS256, RS384, or RS512.
-- @param header Additional values to put in the JWT header.
-- @param The resulting JWT token, or nil and an error message.
function M.encode(data, key, alg, header)
if type(data) ~= 'table' then return nil, "Argument #1 must be table" end
if type(key) ~= 'string' then return nil, "Argument #2 must be string" end
alg = alg or "HS256"
if not alg_sign[alg] then
return nil, "Algorithm not supported"
end
header = header or {}
header['typ'] = 'JWT'
header['alg'] = alg
local headerEncoded, err = cjson_safe.encode(header)
if headerEncoded == nil then
return nil, err
end
local dataEncoded, err = cjson_safe.encode(data)
if dataEncoded == nil then
return nil, err
end
local segments = {
basexx.to_url64(headerEncoded),
basexx.to_url64(dataEncoded)
}
local signing_input = table.concat(segments, ".")
local signature, error = alg_sign[alg](signing_input, key)
if signature == nil then
return nil, error
end
segments[#segments+1] = basexx.to_url64(signature)
return table.concat(segments, ".")
end
-- Verify that the token is valid, and if it is return the decoded JSON payload data.
-- @param token The token to verify.
-- @param expectedAlgo The signature algorithm the caller expects the token to be signed with:
-- HS256, HS384, HS512, RS256, RS384, or RS512.
-- @param key The verification key used for the signature.
-- @param acceptedIssuers Optional table of accepted issuers. If not nil, the 'iss' claim will be
-- checked against this list.
-- @param acceptedAudiences Optional table of accepted audiences. If not nil, the 'aud' claim will
-- be checked against this list.
-- @return A table representing the JSON body of the token, or nil and an error message.
function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
if type(token) ~= 'string' then return nil, "token argument must be string" end
if type(expectedAlgo) ~= 'string' then return nil, "algorithm argument must be string" end
if type(key) ~= 'string' then return nil, "key argument must be string" end
if acceptedIssuers ~= nil and type(acceptedIssuers) ~= 'table' then
return nil, "acceptedIssuers argument must be table"
end
if acceptedAudiences ~= nil and type(acceptedAudiences) ~= 'table' then
return nil, "acceptedAudiences argument must be table"
end
if not alg_verify[expectedAlgo] then
return nil, "Algorithm not supported"
end
local header, body, sig, err = parse_token(token)
if err ~= nil then
return nil, err
end
-- Validate header
if not header.typ or header.typ ~= "JWT" then
return nil, "Invalid typ"
end
if not header.alg or header.alg ~= expectedAlgo then
return nil, "Invalid or incorrect alg"
end
-- Validate signature
if not alg_verify[expectedAlgo](strip_signature(token), sig, key) then
return nil, 'Invalid signature'
end
-- Validate body
if body.exp and type(body.exp) ~= "number" then
return nil, "exp must be number"
end
if body.nbf and type(body.nbf) ~= "number" then
return nil, "nbf must be number"
end
if body.exp and os.time() >= body.exp then
return nil, "Not acceptable by exp"
end
if body.nbf and os.time() < body.nbf then
return nil, "Not acceptable by nbf"
end
if acceptedIssuers ~= nil then
local issClaim = body.iss;
if issClaim == nil then
return nil, "'iss' claim is missing";
end
if not verify_claim(issClaim, acceptedIssuers) then
return nil, "invalid 'iss' claim";
end
end
if acceptedAudiences ~= nil then
local audClaim = body.aud;
if audClaim == nil then
return nil, "'aud' claim is missing";
end
if not verify_claim(audClaim, acceptedAudiences) then
return nil, "invalid 'aud' claim";
end
end
return body
end
return M