forked from YunoHost/SSOwat
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaccess.lua
More file actions
427 lines (361 loc) · 13.8 KB
/
Copy pathaccess.lua
File metadata and controls
427 lines (361 loc) · 13.8 KB
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
--
-- access.lua
--
-- This file is executed at every request on a protected domain or server.
-- You just have to read this file normally to understand how and when the
-- request is handled: redirected, forbidden, bypassed or served.
--
-- Initialize and get configuration
local conf = config.get_config()
-- Import helpers
local hlp = require "helpers"
-- Store the request data
req_data = {
request_uri = ngx.var.request_uri,
uri = ngx.var.uri,
https = ngx.var.proxy_https or ngx.var.https,
host = ngx.var.host,
request_method = ngx.var.request_method,
http_referer = ngx.var.http_referer
}
if req_data["https"] and req_data["https"] ~= "" then
req_data["scheme"] = "https"
else
req_data["scheme"] = "http"
end
hlp.set_req_data(req_data)
-- Get the `cache` persistent shared table
local cache = ngx.shared.cache
-- Generate a unique token if it has not been generated yet
srvkey = cache:get("srvkey")
if not srvkey then
srvkey = random_string()
cache:add("srvkey", srvkey)
end
-- Just a note for the client to know that he passed through the SSO
ngx.header["X-SSO-WAT"] = "You've just been SSOed"
--
-- 0. LOGOUT if requested, but only if logged in
--
local logout_ck = ngx.var.cookie_SSOwFullLogout
if logout_ck and logout_ck ~= "" and hlp.is_logged_in() then
return hlp.logout()
end
--
-- 1. LOGIN
--
-- example: https://mydomain.org/?sso_login=a6e5320f
--
-- If the `sso_login` URI argument is set, try a cross-domain authentication
-- with the token passed as argument
--
if req_data["host"] ~= conf["portal_domain"] and req_data["request_method"] == "GET" then
uri_args = ngx.req.get_uri_args()
if uri_args[conf.login_arg] then
cda_key = uri_args[conf.login_arg]
-- Use the `cache` shared table where a username is associated with
-- a CDA key
user = cache:get("CDA|"..cda_key)
if user then
hlp.set_auth_cookie(user, req_data["host"])
ngx.log(ngx.NOTICE, "Cross-domain authentication: "..user.." connected on "..req_data["host"])
cache:delete("CDA|"..cda_key)
end
uri_args[conf.login_arg] = nil
return hlp.redirect(req_data['request_uri'])
end
end
--
-- 2. PORTAL
--
-- example: https://mydomain.org/ssowat*
--
-- If the URL matches the portal URL, serve a portal file or proceed to a
-- portal operation
--
if req_data["host"] == conf["portal_domain"]
and hlp.string.starts(req_data['request_uri'], string.sub(conf["portal_path"], 1, -2))
then
-- `GET` method will serve a portal file
if req_data["request_method"] == "GET" then
-- Add a trailing `/` if not present
if req_data['request_uri'].."/" == conf["portal_path"] then
ngx.log(ngx.DEBUG, "REDIRECT MISSING /")
return hlp.redirect(conf.portal_url)
end
-- Get request arguments
uri_args = ngx.req.get_uri_args()
-- Logout is also called via a `GET` method
-- TODO: change this ?
if uri_args.action and uri_args.action == 'logout' then
return hlp.logout()
-- If the `r` URI argument is set, it means that we want to
-- be redirected (typically after a login phase)
elseif hlp.is_logged_in() and uri_args.r then
-- Decode back url
back_url = ngx.decode_base64(uri_args.r)
-- If `back_url` contains line break, someone is probably trying to
-- pass some additional headers
if string.match(back_url, "(.*)\n") then
hlp.flash("fail", hlp.t("redirection_error_invalid_url"))
ngx.log(ngx.ERR, "Redirection url is invalid")
ngx.log(ngx.DEBUG, "REDIRECT \\N FOUND")
return hlp.redirect(conf.portal_url)
end
-- Get managed domains
conf = config.get_config()
local managed_domain = false
for _, domain in ipairs(conf["domains"]) do
local escaped_domain = domain:gsub("-", "%%-") -- escape dash for pattern matching
if string.match(back_url, "^http[s]?://"..escaped_domain.."/") then
ngx.log(ngx.INFO, "Redirection to a managed domain found")
managed_domain = true
break
end
end
-- If redirection does not match one of the managed domains
-- redirect to portal home page
if not managed_domain then
hlp.flash("fail", hlp.t("redirection_error_unmanaged_domain"))
ngx.log(ngx.ERR, "Redirection to an external domain aborted")
return hlp.redirect(conf.portal_url)
end
-- In case the `back_url` is not on the same domain than the
-- current one, create a redirection with a CDA key
local ngx_host_escaped = req_data["host"]:gsub("-", "%%-") -- escape dash for pattern matching
if not string.match(back_url, "^http[s]?://"..ngx_host_escaped.."/")
and not string.match(back_url, ".*"..conf.login_arg.."=%d+$") then
local cda_key = hlp.set_cda_key()
if string.match(back_url, ".*?.*") then
back_url = back_url.."&"
else
back_url = back_url.."?"
end
back_url = back_url.."sso_login="..cda_key
end
ngx.log(ngx.DEBUG, "REDIRECT BACK URL REQUESTED")
return hlp.redirect(back_url)
-- In case we want to serve portal login or assets for portal, just
-- serve it
elseif hlp.is_logged_in()
or req_data['request_uri'] == conf["portal_path"]
or hlp.string.starts(req_data['request_uri'], conf["portal_path"].."?")
or (hlp.string.starts(req_data['request_uri'], conf["portal_path"].."assets")
and (not req_data["http_referer"]
or hlp.string.starts(req_data["http_referer"], conf.portal_url)))
then
local uri = req_data['request_uri']
local i, _ = string.find(uri, '?')
if i then
uri = string.sub(uri, 1, i-1)
end
return hlp.serve(uri)
-- If all the previous cases have failed, redirect to portal
else
hlp.flash("info", hlp.t("please_login"))
ngx.log(ngx.DEBUG, "REDIRECT GET CATCHALL…")
return hlp.redirect(conf.portal_url)
end
-- `POST` method is basically use to achieve editing operations
elseif req_data["request_method"] == "POST" then
-- CSRF protection, only proceed if we are editing from the same
-- domain
if hlp.string.starts(req_data["http_referer"], conf.portal_url) then
if hlp.string.ends(req_data["uri"], conf["portal_path"].."password.html")
or hlp.string.ends(req_data["uri"], conf["portal_path"].."edit.html")
then
return hlp.edit_user()
else
return hlp.login()
end
else
-- Redirect to portal
hlp.flash("fail", hlp.t("please_login_from_portal"))
ngx.log(ngx.DEBUG, "REDIRECT POST CATCHALL…")
return hlp.redirect(conf.portal_url)
end
end
end
--
-- 3. Redirected URLs
--
-- If the URL matches one of the `redirected_urls` in the configuration file,
-- just redirect to the target URL/URI
--
function detect_redirection(redirect_url)
if hlp.string.starts(redirect_url, "http://")
or hlp.string.starts(redirect_url, "https://") then
return hlp.redirect(redirect_url)
elseif hlp.string.starts(redirect_url, "/") then
return hlp.redirect(req_data["scheme"].."://"..req_data["host"]..redirect_url)
else
return hlp.redirect(req_data["scheme"].."://"..redirect_url)
end
end
if conf["redirected_urls"] then
for url, redirect_url in pairs(conf["redirected_urls"]) do
if url == req_data["host"]..req_data['request_uri']
or url == req_data["scheme"].."://"..req_data["host"]..req_data['request_uri']
or url == req_data['request_uri'] then
detect_redirection(redirect_url)
end
end
end
if conf["redirected_regex"] then
for regex, redirect_url in pairs(conf["redirected_regex"]) do
if string.match(req_data["host"]..req_data['request_uri'], regex)
or string.match(req_data["scheme"].."://"..req_data["host"]..req_data['request_uri'], regex)
or string.match(req_data['request_uri'], regex) then
detect_redirection(redirect_url)
end
end
end
--
-- 4. Protected URLs
--
-- If the URL matches one of the `protected_urls` in the configuration file,
-- we have to protect it even if the URL is also set in the `unprotected_urls`.
-- It could be useful if you want to unprotect every URL except a few
-- particular ones.
--
function is_protected()
if not conf["protected_urls"] then
conf["protected_urls"] = {}
end
if not conf["protected_regex"] then
conf["protected_regex"] = {}
end
for _, url in ipairs(conf["protected_urls"]) do
if hlp.string.starts(req_data["host"]..req_data['request_uri'], url)
or hlp.string.starts(req_data['request_uri'], url) then
return true
end
end
for _, regex in ipairs(conf["protected_regex"]) do
if string.match(req_data["host"]..req_data['request_uri'], regex)
or string.match(req_data['request_uri'], regex) then
return true
end
end
return false
end
--
-- 5. Skipped URLs
--
-- If the URL matches one of the `skipped_urls` in the configuration file,
-- it means that the URL should not be protected by the SSO and no header
-- has to be sent, even if the user is already authenticated.
--
if conf["skipped_urls"] then
for _, url in ipairs(conf["skipped_urls"]) do
if (hlp.string.starts(req_data["host"]..req_data['request_uri'], url)
or hlp.string.starts(req_data['request_uri'], url))
and not is_protected() then
return hlp.pass()
end
end
end
if conf["skipped_regex"] then
for _, regex in ipairs(conf["skipped_regex"]) do
if (string.match(req_data["host"]..req_data['request_uri'], regex)
or string.match(req_data['request_uri'], regex))
and not is_protected() then
return hlp.pass()
end
end
end
--
-- 6. Specific files (used in YunoHost)
--
-- We want to serve specific portal assets right at the root of the domain.
--
if hlp.is_logged_in() then
if string.match(req_data['request_uri'], "^/ynhpanel.js$") then
hlp.serve(conf["portal_path"].."assets/js/ynhpanel.js")
end
if string.match(req_data['request_uri'], "^/ynhpanel.css$") then
hlp.serve(conf["portal_path"].."assets/css/ynhpanel.css")
end
if string.match(req_data['request_uri'], "^/ynhpanel.json$") then
hlp.serve(conf["portal_path"].."assets/js/ynhpanel.json")
end
-- If user has no access to this URL, redirect him to the portal
if not hlp.has_access() then
ngx.log(ngx.DEBUG, "REDIRECT ACCESS DENIED")
return hlp.redirect(conf.portal_url)
end
-- If the user is authenticated and has access to the URL, set the headers
-- and let it be
hlp.set_headers()
return hlp.pass()
end
--
-- 7. Unprotected URLs
--
-- If the URL matches one of the `unprotected_urls` in the configuration file,
-- it means that the URL should not be protected by the SSO *but* headers have
-- to be sent if the user is already authenticated.
--
-- It means that you can let anyone access to an app, but if a user has already
-- been authenticated on the portal, he can have his authentication headers
-- passed to the app.
--
if conf["unprotected_urls"] then
for _, url in ipairs(conf["unprotected_urls"]) do
if (hlp.string.starts(req_data["host"]..req_data['request_uri'], url)
or hlp.string.starts(req_data['request_uri'], url))
and not is_protected() then
if hlp.is_logged_in() then
hlp.set_headers()
end
return hlp.pass()
end
end
end
if conf["unprotected_regex"] then
for _, regex in ipairs(conf["unprotected_regex"]) do
if (string.match(req_data["host"]..req_data['request_uri'], regex)
or string.match(req_data['request_uri'], regex))
and not is_protected() then
if hlp.is_logged_in() then
hlp.set_headers()
end
return hlp.pass()
end
end
end
--
-- 8. Basic HTTP Authentication
--
-- If the `Authorization` header is set before reaching the SSO, we want to
-- match user and password against the user database.
--
-- It allows you to bypass the cookie-based procedure with a per-request
-- authentication. Very usefull when you are trying to reach a specific URL
-- via cURL for example.
--
local auth_header = ngx.req.get_headers()["Authorization"]
if auth_header then
_, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$")
_, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$")
user = hlp.authenticate(user, password)
if user then
hlp.set_headers(user)
-- If user has no access to this URL, redirect him to the portal
if not hlp.has_access(user) then
ngx.log(ngx.DEBUG, "REDIRECT BASIC AUTH OK")
return hlp.redirect(conf.portal_url)
end
return hlp.pass()
end
end
--
-- 9. Redirect to login
--
-- If no previous rule has matched, just redirect to the portal login.
-- The default is to protect every URL by default.
--
hlp.flash("info", hlp.t("please_login"))
local back_url = req_data["scheme"] .. "://" .. req_data["host"] .. req_data['request_uri']
ngx.log(ngx.DEBUG, "REDIRECT BY DEFAULT")
return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))