forked from platinummonkey/nginx-jwt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnginx-jwt.lua
217 lines (181 loc) · 6.48 KB
/
nginx-jwt.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
local jwt = require "resty.jwt"
local cjson = require "cjson"
local basexx = require "basexx"
local logToken = os.getenv("LOG_TOKEN")
if logToken == nil then
logToken = false
else
logToken = string.lower(logToken)
if logToken == "true" or logToken == "1" then
logToken = true
else
logToken = false
end
end
local secret = os.getenv("JWT_SECRET")
local authHeader = os.getenv("AUTHORIZATION_HEADER")
if authHeader == "" or authHeader == nil then
authHeader = "Authorization"
end
local authTokenPrefix = os.getenv("AUTHORIZATION_PREFIX")
if authTokenPrefix == nil then
authTokenPrefix = "Bearer"
end
assert(secret ~= nil, "Environment variable JWT_SECRET not set")
if os.getenv("JWT_SECRET_IS_BASE64_ENCODED") == 'true' then
-- convert from URL-safe Base64 to Base64
local r = #secret % 4
if r == 2 then
secret = secret .. "=="
elseif r == 3 then
secret = secret .. "="
end
secret = string.gsub(secret, "-", "+")
secret = string.gsub(secret, "_", "/")
-- convert from Base64 to UTF-8 string
secret = basexx.from_base64(secret)
end
local M = {}
function M.auth(claim_specs, header_specs)
-- require Authorization request header
local auth_header = ngx.req.get_headers()[authHeader]
if auth_header == nil then
ngx.log(ngx.WARN, "No Authorization header")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if (logToken ~= false) then
ngx.log(ngx.INFO, "Authorization: " .. auth_header)
end
-- require Bearer token
local token;
if authTokenPrefix ~= nil and authTokenPrefix ~= "" then
local _
_, _, token = string.find(auth_header, authTokenPrefix .. "%s+(.+)")
else
token = auth_header
end
if token == nil then
ngx.log(ngx.WARN, "Missing token")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if (logToken ~= false) then
ngx.log(ngx.INFO, "Token: " .. token)
end
-- require valid JWT
local jwt_obj = jwt:verify(secret, token, 0)
if jwt_obj.verified == false then
ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if (logToken ~= false) then
ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj))
end
-- optionally require specific claims
if claim_specs ~= nil then
-- make sure they passed a Table
if type(claim_specs) ~= 'table' then
ngx.log(ngx.STDERR, "Configuration error: claim_specs arg must be a table")
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- process each claim
local blocking_claim = ""
local spec_actions
for claim, spec in pairs(claim_specs) do
-- make sure token actually contains the claim
local claim_value = jwt_obj.payload[claim]
if claim_value == nil then
blocking_claim = claim .. " (missing)"
break
end
spec_actions = spec_actions or {
-- claim spec is a string (pattern)
["string"] = function (pattern, val)
return string.match(val, pattern) ~= nil
end,
-- claim spec is a predicate function
["function"] = function (func, val)
-- convert truthy to true/false
if func(val) then
return true
else
return false
end
end
}
local spec_action = spec_actions[type(spec)]
-- make sure claim spec is a supported type
if spec_action == nil then
ngx.log(ngx.STDERR, "Configuration error: claim_specs arg claim '" .. claim .. "' must be a string or a function")
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- make sure token claim value satisfies the claim spec
if not spec_action(spec, claim_value) then
blocking_claim = claim
break
end
end
if blocking_claim ~= "" then
ngx.log(ngx.WARN, "User did not satisfy claim: ".. blocking_claim)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
end
-- optionally add specific headers
if header_specs ~= nil then
-- make sure they passed a Table
if type(header_specs) ~= 'table' then
ngx.log(ngx.STDERR, "Configuration error: header_specs arg must be a table")
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
local blocking_claim = ""
local spec_actions
for claim, spec in pairs(header_specs) do
local claim_value = jwt_obj.payload[claim]
spec_actions = spec_actions or {
-- claim spec is a string
["string"] = function (header, val)
return header;
end,
-- claim spec is a predicate function
["function"] = function (func, val)
return func(val)
end
}
local spec_action = spec_actions[type(spec)]
-- make sure claim spec is a supported type
if spec_action == nil then
ngx.log(ngx.STDERR, "Configuration error: header_specs arg claim '" .. claim .. "' must be a string or a function")
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- make sure token claim value satisfies the claim spec
local header = spec_action(spec, claim_value)
if header ~= nil and string.sub(header, 1, 1) == "~" then
-- optional header if empty string, ignore
ngx.req.clear_header(string.sub(header, 2))
elseif header == nil or (header ~= nil and claim_value == nil) then
blocking_claim = claim .. " (missing)"
else
ngx.req.set_header(header, claim_value)
end
end
if blocking_claim ~= "" then
ngx.log(ngx.WARN, "User did not satisfy claim: ".. blocking_claim)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
end
end
function M.table_contains(table, item)
for _, value in pairs(table) do
if value == item then return true end
end
return false
end
function M.make_optional_header(header)
return function (val)
if val ~= nil then
return header
else
return "~" .. header
end
end
end
return M