-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
hmac-auth.lua
457 lines (378 loc) · 13.8 KB
/
hmac-auth.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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local ngx = ngx
local type = type
local abs = math.abs
local ngx_time = ngx.time
local ngx_re = require("ngx.re")
local ngx_req = ngx.req
local pairs = pairs
local ipairs = ipairs
local hmac_sha1 = ngx.hmac_sha1
local escape_uri = ngx.escape_uri
local core = require("apisix.core")
local hmac = require("resty.hmac")
local consumer = require("apisix.consumer")
local plugin = require("apisix.plugin")
local ngx_decode_base64 = ngx.decode_base64
local ngx_encode_base64 = ngx.encode_base64
local BODY_DIGEST_KEY = "X-HMAC-DIGEST"
local SIGNATURE_KEY = "X-HMAC-SIGNATURE"
local ALGORITHM_KEY = "X-HMAC-ALGORITHM"
local DATE_KEY = "Date"
local ACCESS_KEY = "X-HMAC-ACCESS-KEY"
local SIGNED_HEADERS_KEY = "X-HMAC-SIGNED-HEADERS"
local plugin_name = "hmac-auth"
local MAX_REQ_BODY = 1024 * 512
local lrucache = core.lrucache.new({
type = "plugin",
})
local schema = {
type = "object",
title = "work with route or service object",
properties = {},
}
local consumer_schema = {
type = "object",
title = "work with consumer object",
properties = {
access_key = {type = "string", minLength = 1, maxLength = 256},
secret_key = {type = "string", minLength = 1, maxLength = 256},
algorithm = {
type = "string",
enum = {"hmac-sha1", "hmac-sha256", "hmac-sha512"},
default = "hmac-sha256"
},
clock_skew = {
type = "integer",
default = 0
},
signed_headers = {
type = "array",
items = {
type = "string",
minLength = 1,
maxLength = 50,
}
},
keep_headers = {
type = "boolean",
title = "whether to keep the http request header",
default = false,
},
encode_uri_params = {
type = "boolean",
title = "Whether to escape the uri parameter",
default = true,
},
validate_request_body = {
type = "boolean",
title = "A boolean value telling the plugin to enable body validation",
default = false,
},
max_req_body = {
type = "integer",
title = "Max request body size",
default = MAX_REQ_BODY,
},
},
required = {"access_key", "secret_key"},
}
local _M = {
version = 0.1,
priority = 2530,
type = 'auth',
name = plugin_name,
schema = schema,
consumer_schema = consumer_schema
}
local hmac_funcs = {
["hmac-sha1"] = function(secret_key, message)
return hmac_sha1(secret_key, message)
end,
["hmac-sha256"] = function(secret_key, message)
return hmac:new(secret_key, hmac.ALGOS.SHA256):final(message)
end,
["hmac-sha512"] = function(secret_key, message)
return hmac:new(secret_key, hmac.ALGOS.SHA512):final(message)
end,
}
local function array_to_map(arr)
local map = core.table.new(0, #arr)
for _, v in ipairs(arr) do
map[v] = true
end
return map
end
local function remove_headers(ctx, ...)
local headers = { ... }
if headers and #headers > 0 then
for _, header in ipairs(headers) do
core.log.info("remove_header: ", header)
core.request.set_header(ctx, header, nil)
end
end
end
local create_consumer_cache
do
local consumer_names = {}
function create_consumer_cache(consumers)
core.table.clear(consumer_names)
for _, consumer in ipairs(consumers.nodes) do
core.log.info("consumer node: ", core.json.delay_encode(consumer))
consumer_names[consumer.auth_conf.access_key] = consumer
end
return consumer_names
end
end -- do
function _M.check_schema(conf, schema_type)
core.log.info("input conf: ", core.json.delay_encode(conf))
if schema_type == core.schema.TYPE_CONSUMER then
return core.schema.check(consumer_schema, conf)
else
return core.schema.check(schema, conf)
end
end
local function get_consumer(access_key)
if not access_key then
return nil, {message = "missing access key"}
end
local consumer_conf = consumer.plugin(plugin_name)
if not consumer_conf then
return nil, {message = "Missing related consumer"}
end
local consumers = lrucache("consumers_key", consumer_conf.conf_version,
create_consumer_cache, consumer_conf)
local consumer = consumers[access_key]
if not consumer then
return nil, {message = "Invalid access key"}
end
core.log.info("consumer: ", core.json.delay_encode(consumer))
return consumer
end
local function get_conf_field(access_key, field_name)
local consumer, err = get_consumer(access_key)
if err then
return false, err
end
return consumer.auth_conf[field_name]
end
local function do_nothing(v)
return v
end
local function generate_signature(ctx, secret_key, params)
local canonical_uri = ctx.var.uri
local canonical_query_string = ""
local request_method = ngx_req.get_method()
local args = ngx_req.get_uri_args()
if canonical_uri == "" then
canonical_uri = "/"
end
if type(args) == "table" then
local keys = {}
local query_tab = {}
for k, v in pairs(args) do
core.table.insert(keys, k)
end
core.table.sort(keys)
local field_val = get_conf_field(params.access_key, "encode_uri_params")
core.log.info("encode_uri_params: ", field_val)
local encode_or_not = do_nothing
if field_val then
encode_or_not = escape_uri
end
for _, key in pairs(keys) do
local param = args[key]
-- when args without `=<value>`, value is treated as true.
-- In order to be compatible with args lacking `=<value>`,
-- we need to replace true with an empty string.
if type(param) == "boolean" then
param = ""
end
-- whether to encode the uri parameters
if type(param) == "table" then
for _, val in pairs(param) do
core.table.insert(query_tab, encode_or_not(key) .. "=" .. encode_or_not(val))
end
else
core.table.insert(query_tab, encode_or_not(key) .. "=" .. encode_or_not(param))
end
end
canonical_query_string = core.table.concat(query_tab, "&")
end
core.log.info("all headers: ",
core.json.delay_encode(core.request.headers(ctx), true))
local signing_string_items = {
request_method,
canonical_uri,
canonical_query_string,
params.access_key,
params.date,
}
if params.signed_headers then
for _, h in ipairs(params.signed_headers) do
local canonical_header = core.request.header(ctx, h) or ""
core.table.insert(signing_string_items,
h .. ":" .. canonical_header)
core.log.info("canonical_header name:", core.json.delay_encode(h))
core.log.info("canonical_header value: ",
core.json.delay_encode(canonical_header))
end
end
local signing_string = core.table.concat(signing_string_items, "\n") .. "\n"
core.log.info("signing_string: ", signing_string,
" params.signed_headers:",
core.json.delay_encode(params.signed_headers))
return hmac_funcs[params.algorithm](secret_key, signing_string)
end
local function validate(ctx, params)
if not params.access_key or not params.signature then
return nil, {message = "access key or signature missing"}
end
if not params.algorithm then
return nil, {message = "algorithm missing"}
end
local consumer, err = get_consumer(params.access_key)
if err then
return nil, err
end
local conf = consumer.auth_conf
if conf.algorithm ~= params.algorithm then
return nil, {message = "algorithm " .. params.algorithm .. " not supported"}
end
core.log.info("clock_skew: ", conf.clock_skew)
if conf.clock_skew and conf.clock_skew > 0 then
local time = ngx.parse_http_time(params.date)
core.log.info("params.date: ", params.date, " time: ", time)
if not time then
return nil, {message = "Invalid GMT format time"}
end
local diff = abs(ngx_time() - time)
core.log.info("gmt diff: ", diff)
if diff > conf.clock_skew then
return nil, {message = "Clock skew exceeded"}
end
end
-- validate headers
if conf.signed_headers and #conf.signed_headers >= 1 then
local headers_map = array_to_map(conf.signed_headers)
if params.signed_headers then
for _, header in ipairs(params.signed_headers) do
if not headers_map[header] then
return nil, {message = "Invalid signed header " .. header}
end
end
end
end
local secret_key = conf and conf.secret_key
local request_signature = ngx_decode_base64(params.signature)
local generated_signature = generate_signature(ctx, secret_key, params)
core.log.info("request_signature: ", request_signature,
" generated_signature: ", generated_signature)
if request_signature ~= generated_signature then
return nil, {message = "Invalid signature"}
end
local validate_request_body = get_conf_field(params.access_key, "validate_request_body")
if validate_request_body then
local digest_header = params.body_digest
if not digest_header then
return nil, {message = "Invalid digest"}
end
local max_req_body = get_conf_field(params.access_key, "max_req_body")
local req_body, err = core.request.get_body(max_req_body, ctx)
if err then
return nil, {message = "Exceed body limit size"}
end
req_body = req_body or ""
local request_body_hash = ngx_encode_base64(
hmac_funcs[params.algorithm](secret_key, req_body))
if request_body_hash ~= digest_header then
return nil, {message = "Invalid digest"}
end
end
return consumer
end
local function get_params(ctx)
local params = {}
local access_key = ACCESS_KEY
local signature_key = SIGNATURE_KEY
local algorithm_key = ALGORITHM_KEY
local date_key = DATE_KEY
local signed_headers_key = SIGNED_HEADERS_KEY
local body_digest_key = BODY_DIGEST_KEY
local attr = plugin.plugin_attr(plugin_name)
if attr then
access_key = attr.access_key or access_key
signature_key = attr.signature_key or signature_key
algorithm_key = attr.algorithm_key or algorithm_key
date_key = attr.date_key or date_key
signed_headers_key = attr.signed_headers_key or signed_headers_key
body_digest_key = attr.body_digest_key or body_digest_key
end
local app_key = core.request.header(ctx, access_key)
local signature = core.request.header(ctx, signature_key)
local algorithm = core.request.header(ctx, algorithm_key)
local date = core.request.header(ctx, date_key)
local signed_headers = core.request.header(ctx, signed_headers_key)
local body_digest = core.request.header(ctx, body_digest_key)
core.log.info("signature_key: ", signature_key)
-- get params from header `Authorization`
if not app_key then
local auth_string = core.request.header(ctx, "Authorization")
if not auth_string then
return params
end
local auth_data = ngx_re.split(auth_string, "#")
core.log.info("auth_string: ", auth_string, " #auth_data: ",
#auth_data, " auth_data: ",
core.json.delay_encode(auth_data))
if #auth_data == 6 and auth_data[1] == "hmac-auth-v1" then
app_key = auth_data[2]
signature = auth_data[3]
algorithm = auth_data[4]
date = auth_data[5]
signed_headers = auth_data[6]
end
end
params.access_key = app_key
params.algorithm = algorithm
params.signature = signature
params.date = date or ""
params.signed_headers = signed_headers and ngx_re.split(signed_headers, ";")
params.body_digest = body_digest
local keep_headers = get_conf_field(params.access_key, "keep_headers")
core.log.info("keep_headers: ", keep_headers)
if not keep_headers then
remove_headers(ctx, signature_key, algorithm_key, signed_headers_key)
end
core.log.info("params: ", core.json.delay_encode(params))
return params
end
function _M.rewrite(conf, ctx)
local params = get_params(ctx)
local validated_consumer, err = validate(ctx, params)
if err then
return 401, err
end
if not validated_consumer then
return 401, {message = "Invalid signature"}
end
local consumer_conf = consumer.plugin(plugin_name)
consumer.attach_consumer(ctx, validated_consumer, consumer_conf)
core.log.info("hit hmac-auth rewrite")
end
return _M