-
Notifications
You must be signed in to change notification settings - Fork 2
/
sso-auth.lua
392 lines (360 loc) · 12.3 KB
/
sso-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
local sso_auth = { }
local function init()
local handle, msg, err = io.open("/dev/urandom", "r")
if handle then
ngx.shared.sso:set("secret", ngx.encode_base64(ngx.sha1_bin(handle:read(32))))
handle:close()
else
ngx.log(ngx.ERR, "Cannot access \"/dev/urandom\"; SSO authentication will not work")
end
end
function sso_auth.GetRespCookies()
local cookies = ngx.header["Set-Cookie"]
if cookies then
if type(cookies) ~= "table" then
cookies = { cookies }
ngx.header["Set-Cookie"] = cookies
end
else
cookies = { }
end
return cookies
end
function sso_auth.GetRespCookie(cookie)
local cookies = sso_auth.GetRespCookies()
local key = cookie:gsub("=.*", "")
for i, val in ipairs(cookies) do
if key == val:gsub("=.*", "") then
return val, i, cookies
end
end
return nil, #cookies + 1, cookies
end
function sso_auth.SetCookie(cookie, delay)
if delay then
local cookies = ngx.ctx.sso_set_cookie
if not cookies then
cookies = { }
end
cookies[cookie:gsub("=.*", "")] = cookie:gsub("^[^=]*=", "")
ngx.ctx.sso_set_cookie = cookies
else
local val, i, cookies = sso_auth.GetRespCookie(cookie)
cookies[i] = cookie
ngx.header["Set-Cookie"] = cookies
end
end
function sso_auth.ExpireCookie(cookie, delay)
sso_auth.SetCookie(cookie:gsub("=.*", "") ..
"=; path=/; expires=Thu, 1 Jan 1970 00:00:00 UTC",
delay)
end
local function FlattenRespCookies()
local delayed = ngx.ctx.sso_set_cookie
if not delayed then
return
end
local cookies = sso_auth.GetRespCookies()
for i, val in ipairs(cookies) do
local k = val:gsub("=.*", "")
if not delayed[k] then
delayed[k] = val:gsub("^[^=]*=", "")
end
end
cookies = { }
for k, v in pairs(delayed) do
cookies[#cookies + 1] = k .. "=" .. v
end
ngx.header["Set-Cookie"] = cookies
ngx.ctx.sso_set_cookie = nil
end
function sso_auth.challenge()
-- Retrieve global "secret" to be used for signing
local key = ngx.shared.sso:get("secret")
if not key or key == "" then
ngx.log(ngx.ERR, "sso:secret was not initialized by init-auth.conf")
return ngx.exit(ngx.ERROR)
end
-- Obtain the current time, sign it, and use it as a challenge for the login
-- web page.
-- First, convert current time to binary representation.
local tm = math.abs(ngx.time())
local raw = ""
for i = 1, 4 do
raw = raw .. string.char(tm % 256)
tm = tm / 256
end
-- Then encode current time and HMAC-SHA1 signature as a single Base64 encoded
-- string.
-- This results in a string that is exactly 35 characters long (including
-- surrounding quotes and semicolon).
tm = "\"" .. ngx.encode_base64(raw .. ngx.hmac_sha1(key, raw)) .. "\";"
if tm:len() ~= 35 then return ngx.exit(ngx.ERROR) end
-- Insert the parameter into the HTML source. Try to keep the existing length
-- of the file, so that we don't need to worry about recomputing Content-Length.
ngx.arg[1] = ngx.arg[1]:
gsub("(challenge%s*=%s*)\"" .. string.rep("%d", 32) .. "\";", "%1" .. tm, 1)
end
function sso_auth.csrfProtection(url)
local ref = ngx.req.get_headers()["Referer"];
if ref and not ref:match("^" .. url) then
ngx.header["Content-Type"] = "text/html"
ngx.header["Refresh"] = "0;URL=" .. url
return ngx.exit(200)
end
end
function sso_auth.access()
local SESSION_TIMEOUT = 60*60
local LOGIN_TIMEOUT = 15*60
-- Retrieve global "secret" to be used for signing
local key = ngx.shared.sso:get("secret")
if not key or key == "" then
ngx.log(ngx.ERR, "sso:secret was not initialized by init-auth.conf")
return ngx.exit(ngx.ERROR)
end
-- The caller also provides the realm that should be protected by the
-- SSO authentication.
local realm = ngx.var.sso_realm;
if not realm or realm == "" then
ngx.log(ngx.ERR, "Caller did not set $sso_realm")
return ngx.exit(ngx.ERROR)
end
-- Convert time to four raw bytes
local function TimeToRaw(tm)
tm = math.abs(tm)
local raw = ""
for i = 1, 4 do
raw = raw .. string.char(tm % 256)
tm = tm / 256
end
return raw
end
-- Convert four raw bytes to time
local function RawToTime(raw)
local tm0, tm1, tm2, tm3 = raw:byte(1, 4)
return ((tm3*256 + tm2)*256 + tm1)*256 + tm0
end
-- Read matching line from "auth-sso" file
local function ReadSSO(user)
local handle, msg, err = io.open("/etc/sso-auth", "r")
if not handle then return end
for line in handle:lines() do
local u,h,r = line:
match("([^#]*).*"):
match("^%s*([^%s]*)%s+([^%s]*)%s+([^%s]*).*$")
if u and h and r and u ~= "" and h ~= "" and r ~= "" then
if u == user then
handle:close()
return u, h, r
end
end
end
handle:close()
return
end
-- Retrieve cookie value as table
local function GetSSOCookie()
local cookie = ngx.var.cookie_SSOAuth
if cookie then
local hmac, tm_raw, realms = ngx.decode_base64(cookie):
match("^(" .. string.rep(".", 20) .. ")(....)(.*)$")
if hmac and hmac ~= "" and realms and realms ~= "" and
ngx.hmac_sha1(key, tm_raw .. realms) == hmac then
local diff = ngx.time() - RawToTime(tm_raw)
if diff >= 0 and diff < SESSION_TIMEOUT then
return realms
end
end
end
return nil
end
-- Set the cookie after adding a timestamp and signature
local function SetSSOCookie(realms, force)
if realms and realms ~= "" then
local tm_raw = TimeToRaw(ngx.time())
realms = tm_raw .. realms
local cookie =
"SSOAuth=" ..
ngx.encode_base64(ngx.hmac_sha1(key, realms) .. realms) ..
"; path=/" .. "; HttpOnly"
if ngx.var.scheme == "https" then
cookie = cookie .. "; secure"
end
sso_auth.SetCookie(cookie, not force)
end
end
-- Extend the time that the user is logged into the SSO system
local function ExtendCookieDuration(realm)
local realms = GetSSOCookie()
if realms and realms ~= "" then
for s in realms:gmatch("[^,]+") do
if realm == s then
SetSSOCookie(realms)
return true
end
end
end
return false
end
-- If the user has a valid cookie, allow the request
if ExtendCookieDuration(realm) then
return
end
-- The user submitted a user id and password. Verify the provided information
-- and then decide whether to allow the request.
if ngx.req.get_method() == "POST" then
repeat
-- Read POST arguments. This includes user id, hashed password, and other
-- data.
ngx.req.read_body()
local args, err = ngx.req.get_post_args()
if not args then break end
-- If this was a request for a user's "salt", handle that here.
local user = args["sso_salt_request"]
if user then
local u, h, r = ReadSSO(user)
if u == user then
-- Return the "salt" value in a custom header.
ngx.header["X-Salt"] = h:match("([^:]*)")
else
-- If the user doesn't exist, make-up a fake, but plausible "salt" value.
ngx.header["X-Salt"] = ngx.encode_base64(ngx.hmac_sha1(key, user)):sub(1, 8)
end
return ngx.exit(200)
end
-- Make sure we got all the arguments that we need to make a decision
user = args["sso_auth_user"]
local challenge = args["sso_auth_challenge"]
local password_hash = args["sso_auth_password_hash"]
if not user or user == "" or
not challenge or challenge == "" or
not password_hash or password_hash == "" then
break
end
-- Check signature on signed "challenge"
local tm_raw, tm_hmac = ngx.decode_base64(challenge):match("(....)(.*)")
if ngx.hmac_sha1(key, tm_raw) ~= tm_hmac then break end
-- Compute time when challenge was issued and reconfirm the password if
-- the password dialog has expired
local diff = ngx.time() - RawToTime(tm_raw)
if diff < 0 or diff >= LOGIN_TIMEOUT then
break
end
-- Read "sso-auth" file
local u, h, r = ReadSSO(user)
if u == user then
-- Check if the password matches.
local salt, hash = h:match("([^:]*):(.*)")
local dat1 = ngx.hmac_sha1(ngx.decode_base64(challenge), ngx.decode_base64(hash))
local dat2 = ngx.decode_base64(password_hash)
local data = ""
for i = 1, #dat1 do
data = data .. string.char((256 + dat2:byte(i) - dat1:byte(i)) % 256)
end
if ngx.encode_base64(ngx.sha1_bin(data)) == hash then
-- Check if the user has access to this particular "realm"
for s in r:gmatch("[^,]+") do
if realm == s then
-- The user provided a correct user id and password for this realm;
-- allow the request; and also log the user into all of his realms.
SetSSOCookie(r, true)
ngx.req.set_method(ngx.HTTP_GET)
return
end
end
end
end
until true
end
-- The user is (still) unauthenticated. Inject a login page and ask for
-- credentials.
ngx.req.set_method(ngx.HTTP_GET)
ngx.header["Content-Type"] = "text/html"
return ngx.exec("/auth")
end
function sso_auth.headerFilter(excl)
FlattenRespCookies()
if ngx.header.content_type and
(ngx.header.content_type:find("text/html") or
ngx.header.content_type:find("application/xhtml[+]xml")) then
ngx.header.content_length = nil
end
if not excl or excl["X-Frame-Options"] == nil or excl["X-Frame-Options"] then
ngx.header["X-Frame-Options"] = "SAMEORIGIN"
end
if not excl or excl["X-Content-Type-Options"] == nil or excl["X-Content-Type-Options"] then
ngx.header["X-Content-Type-Options"] = "nosniff"
end
if not excl or excl["X-XSS-Protection"] == nil or excl["X-XSS-Protection"] then
ngx.header["X-XSS-Protection"] = "1; mode=block"
end
end
function sso_auth.bodyFilter()
if ngx.arg[1]:find("<frameset") then
return
end
if ngx.header.content_type and
(ngx.header.content_type:find("text/html") or
ngx.header.content_type:find("application/xhtml[+]xml")) then
ngx.arg[1] = ngx.re.sub(ngx.arg[1], "</head>",
"<style>\
a.sso_auth_overlay {\
background: rgba(240,240,240,0.4) none !important;\
box-shadow: none !important;\
color: transparent !important;\
font-family: Arial, sans-serif !important;\
font-size: 16px !important;\
font-weight: normal !important;\
height: 3px !important;\
opacity: 1 !important;\
overflow-y: hidden !important;\
padding: 0px 20px !important;\
position: fixed !important;\
right: 0px !important;\
text-decoration: none !important;\
text-shadow: #fff 0.1em 0.1em 0.2em !important;\
top: 0px !important;\
z-index: 30000 !important;\
}\
a.sso_auth_overlay:hover {\
background: rgba(240,240,240,0.4) none !important;\
box-shadow: none !important;\
color: #000 !important;\
font-family: Arial, sans-serif !important;\
font-size: 16px !important;\
font-weight: normal !important;\
height: 1em !important;\
opacity: 1 !important;\
overflow-y: visible !important;\
padding: 20px !important;\
position: fixed !important;\
right: 0px !important;\
text-decoration: none !important;\
text-shadow: #fff 0.1em 0.1em 0.2em !important;\
top: 0px !important;\
transition: height 0.15s linear, color 0.15s linear !important;\
z-index: 30000 !important;\
}\
</style>\
</head>\
<a class=\"sso_auth_overlay\" style=\"\
background: rgba(240,240,240,0.2) none;\
box-shadow: none;\
color: transparent;\
font-family: Arial, sans-serif;\
font-size: 16px;\
font-weight: normal;\
height: 3px;\
opacity: 1;\
overflow-y: hidden;\
padding: 0px 20px;\
position: fixed;\
right: 0px;\
text-decoration: none;\
text-shadow: #fff 0.1em 0.1em 0.2em;\
top: 0px;\
z-index: 30000;\" href=\"/logout\" target=\"_top\">Logout</a>");
end
end
init()
return sso_auth