2013-10-15 13:58:16 +02:00
|
|
|
--
|
2015-02-02 00:05:09 +01:00
|
|
|
-- access.lua
|
|
|
|
--
|
|
|
|
-- This file is executed at every request on a protected domain or server.
|
2013-10-15 13:58:16 +02:00
|
|
|
--
|
2014-04-10 20:42:43 +02:00
|
|
|
|
2015-02-02 00:05:09 +01:00
|
|
|
-- Just a note for the client to know that he passed through the SSO
|
2013-10-16 11:57:53 +02:00
|
|
|
ngx.header["X-SSO-WAT"] = "You've just been SSOed"
|
2013-10-15 10:11:39 +02:00
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- Misc imports
|
|
|
|
local jwt = require("vendor.luajwtjitsi.luajwtjitsi")
|
2024-01-30 20:24:08 +01:00
|
|
|
local cipher = require('openssl.cipher')
|
2023-09-27 18:49:28 +02:00
|
|
|
local rex = require("rex_pcre2")
|
2023-11-28 18:04:29 +01:00
|
|
|
local lfs = require("lfs")
|
2015-02-15 12:31:23 +01:00
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
-- 0. Misc helpers because Lua has no sugar ...
|
|
|
|
-- ###########################################################################
|
|
|
|
|
|
|
|
-- Get configuration (we do this here, the conf is re-read every time unless
|
|
|
|
-- the file's timestamp didnt change)
|
|
|
|
local config = require("config")
|
|
|
|
local conf = config.get_config()
|
|
|
|
|
2023-09-02 19:36:18 +02:00
|
|
|
-- Cache expensive calculations
|
|
|
|
local cache = ngx.shared.cache
|
|
|
|
|
|
|
|
-- Hash a string using hmac_sha512, return a hexa string
|
2023-09-27 18:43:13 +02:00
|
|
|
function cached_jwt_verify(data, secret)
|
2023-09-02 19:36:18 +02:00
|
|
|
res = cache:get(data)
|
|
|
|
if res == nil then
|
|
|
|
logger:debug("Result not found in cache, checking login")
|
|
|
|
-- Perform expensive calculation
|
2024-01-30 20:52:23 +01:00
|
|
|
decoded, err = jwt.verify(data, "HS256", COOKIE_SECRET)
|
2023-09-02 19:36:18 +02:00
|
|
|
if not decoded then
|
|
|
|
logger:error(err)
|
2023-11-28 19:57:57 +01:00
|
|
|
return nil, nil, nil, nil, err
|
2023-09-02 19:36:18 +02:00
|
|
|
end
|
|
|
|
-- As explained in set_basic_auth_header(), user and hashed password do not contain ':'
|
2023-11-28 18:04:29 +01:00
|
|
|
-- And cache cannot contain tables, so we use "id:user:password" format
|
2023-11-28 19:57:57 +01:00
|
|
|
cached = decoded['id']..":"..decoded['host']..":"..decoded["user"]..":"..decoded["pwd"]
|
2023-09-02 19:36:18 +02:00
|
|
|
cache:set(data, cached, 120)
|
|
|
|
logger:debug("Result saved in cache")
|
2023-11-28 19:57:57 +01:00
|
|
|
return decoded['id'], decoded['host'], decoded["user"], decoded["pwd"], err
|
2023-09-02 19:36:18 +02:00
|
|
|
else
|
|
|
|
logger:debug("Result found in cache")
|
2023-11-28 19:57:57 +01:00
|
|
|
session_id, host, user, pwd = res:match("([^:]+):([^:]+):([^:]+):(.*)")
|
|
|
|
return session_id, host, user, pwd, nil
|
2023-09-02 19:36:18 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-11-28 19:57:57 +01:00
|
|
|
-- Test whether a string starts/ends with something
|
2023-07-15 19:51:31 +02:00
|
|
|
function string.starts(String, Start)
|
|
|
|
if not String then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
return string.sub(String, 1, string.len(Start)) == Start
|
|
|
|
end
|
|
|
|
|
2023-11-28 19:57:57 +01:00
|
|
|
function string.ends(String, End)
|
|
|
|
if not String then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
return string.sub(String, -string.len(End)) == End
|
|
|
|
end
|
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- Convert a table of arguments to an URI string
|
|
|
|
function uri_args_string(args)
|
|
|
|
if not args then
|
|
|
|
args = ngx.req.get_uri_args()
|
|
|
|
end
|
|
|
|
String = "?"
|
|
|
|
for k,v in pairs(args) do
|
|
|
|
String = String..tostring(k).."="..tostring(v).."&"
|
|
|
|
end
|
|
|
|
return string.sub(String, 1, string.len(String) - 1)
|
|
|
|
end
|
|
|
|
|
2023-09-29 14:30:36 +02:00
|
|
|
function element_is_in_table(element, table)
|
|
|
|
if table then
|
|
|
|
for _, el in pairs(table) do
|
|
|
|
if el == element then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
-- 1. AUTHENTICATION
|
|
|
|
-- Check wether or not this is a logged-in user
|
2023-09-02 19:36:18 +02:00
|
|
|
-- This is not run immediately but only if:
|
|
|
|
-- - the app is not public
|
|
|
|
-- - and/or auth_headers is enabled for this app
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
|
|
|
|
function check_authentication()
|
|
|
|
|
|
|
|
-- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
|
|
|
|
|
|
|
|
local cookie = ngx.var["cookie_" .. conf["cookie_name"]]
|
2024-01-30 20:52:23 +01:00
|
|
|
if cookie == nil or COOKIE_SECRET == nil then
|
2023-09-02 19:36:18 +02:00
|
|
|
return false, nil, nil
|
|
|
|
end
|
2023-07-15 19:51:31 +02:00
|
|
|
|
2024-01-30 20:52:23 +01:00
|
|
|
session_id, host, user, pwd, err = cached_jwt_verify(cookie, COOKIE_SECRET)
|
2023-07-15 19:51:31 +02:00
|
|
|
|
|
|
|
if err ~= nil then
|
|
|
|
return false, nil, nil
|
|
|
|
end
|
2023-11-28 18:04:29 +01:00
|
|
|
|
|
|
|
local session_file = conf["session_folder"] .. '/' .. session_id
|
|
|
|
local session_file_attrs = lfs.attributes(session_file, {"modification"})
|
2023-11-28 18:13:12 +01:00
|
|
|
if session_file_attrs == nil or math.abs(session_file_attrs["modification"] - os.time()) > 3 * 24 * 3600 then
|
2023-11-28 18:04:29 +01:00
|
|
|
-- session expired
|
|
|
|
return false, nil, nil
|
|
|
|
end
|
|
|
|
|
2023-11-28 19:57:57 +01:00
|
|
|
-- Check the host the cookie was meant to does match the request
|
|
|
|
-- (this should never happen except if somehow a malicious user manually tries
|
|
|
|
-- to use a cookie that was delivered from a different domain)
|
2024-01-30 19:05:56 +01:00
|
|
|
if host ~= ngx.var.host and not string.ends(ngx.var.host, "." .. host) then
|
2023-11-28 19:57:57 +01:00
|
|
|
return false, nil, nil
|
|
|
|
end
|
|
|
|
|
2023-11-28 18:04:29 +01:00
|
|
|
return true, user, pwd
|
2023-07-15 19:51:31 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
-- ###########################################################################
|
|
|
|
-- 2. REDIRECTED URLS
|
|
|
|
-- If the URL matches one of the `redirected_urls` in the configuration file,
|
|
|
|
-- just redirect to the target URL/URI
|
|
|
|
-- ###########################################################################
|
|
|
|
|
|
|
|
function convert_to_absolute_url(redirect_url)
|
|
|
|
if string.starts(redirect_url, "http://")
|
|
|
|
or string.starts(redirect_url, "https://") then
|
|
|
|
return redirect_url
|
|
|
|
elseif string.starts(redirect_url, "/") then
|
|
|
|
return ngx.var.scheme.."://"..ngx.var.host..redirect_url
|
2014-04-10 17:35:28 +02:00
|
|
|
else
|
2023-07-15 19:51:31 +02:00
|
|
|
return ngx.var.scheme.."://"..redirect_url
|
2014-04-10 17:35:28 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if conf["redirected_urls"] then
|
|
|
|
for url, redirect_url in pairs(conf["redirected_urls"]) do
|
2023-07-15 19:51:31 +02:00
|
|
|
if url == ngx.var.host..ngx.var.uri..uri_args_string()
|
|
|
|
or url == ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..uri_args_string()
|
|
|
|
or url == ngx.var.uri..uri_args_string() then
|
2023-07-15 21:22:27 +02:00
|
|
|
logger:debug("Found in redirected_urls, redirecting to "..url)
|
|
|
|
ngx.redirect(convert_to_absolute_url(redirect_url))
|
2014-04-10 17:35:28 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
-- 3. IDENTIFY PERMISSION MATCHING THE REQUESTED URL
|
2020-05-21 21:53:04 +02:00
|
|
|
--
|
2020-09-20 17:57:23 +02:00
|
|
|
-- In particular, the conf is filled with permissions such as:
|
2020-05-21 21:53:04 +02:00
|
|
|
--
|
2020-09-20 17:57:23 +02:00
|
|
|
-- "foobar": {
|
|
|
|
-- "auth_header": false,
|
|
|
|
-- "public": false,
|
|
|
|
-- "uris": [
|
|
|
|
-- "yolo.test/foobar",
|
|
|
|
-- "re:^[^/]*/%.well%-known/foobar/.*$",
|
|
|
|
-- ],
|
|
|
|
-- "users": ["alice", "bob"]
|
|
|
|
-- }
|
|
|
|
--
|
|
|
|
--
|
|
|
|
-- And we find the best matching permission by trying to match the request uri
|
|
|
|
-- against all the uris rules/regexes from the conf and keep the longest matching one.
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
2020-05-21 21:53:04 +02:00
|
|
|
|
2024-01-30 20:55:16 +01:00
|
|
|
local permission = nil
|
|
|
|
local longest_match = ""
|
2020-05-21 22:56:52 +02:00
|
|
|
|
2021-01-20 01:28:08 +01:00
|
|
|
ngx_full_url = ngx.var.host..ngx.var.uri
|
|
|
|
|
2020-09-20 17:57:23 +02:00
|
|
|
for permission_name, permission_infos in pairs(conf["permissions"]) do
|
|
|
|
if next(permission_infos['uris']) ~= nil then
|
2023-12-23 20:39:07 +01:00
|
|
|
for _, prefix in pairs(permission_infos['uris']) do
|
|
|
|
local match = nil
|
|
|
|
if string.starts(prefix, "re:") then
|
|
|
|
prefix = string.sub(prefix, 4, string.len(prefix))
|
|
|
|
-- Make sure we match the prefix from the beginning of the url
|
|
|
|
if not string.starts(prefix, "^") then
|
|
|
|
prefix = "^"..prefix
|
|
|
|
end
|
|
|
|
match = rex.match(ngx_full_url, prefix)
|
|
|
|
elseif string.starts(ngx_full_url, prefix) then
|
|
|
|
match = prefix
|
2021-01-20 01:28:08 +01:00
|
|
|
end
|
2020-05-21 22:56:52 +02:00
|
|
|
|
2023-12-23 20:39:07 +01:00
|
|
|
if match ~= nil and string.len(match) > string.len(longest_match) then
|
|
|
|
longest_match = match
|
2020-09-20 17:57:23 +02:00
|
|
|
permission = permission_infos
|
|
|
|
permission["id"] = permission_name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-05-21 21:53:04 +02:00
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
-- 4. CHECK USER HAS ACCESS
|
|
|
|
-- Either because the permission is set as "public: true",
|
|
|
|
-- Or because the logged-in user is listed in the "users" list of the perm
|
|
|
|
-- ###########################################################################
|
|
|
|
|
2024-01-30 20:55:16 +01:00
|
|
|
local has_access
|
|
|
|
|
2023-09-29 14:30:14 +02:00
|
|
|
-- No permission object found = no access
|
|
|
|
if permission == nil then
|
|
|
|
logger:debug("No permission matching request for "..ngx.var.uri.." ... Assuming access is denied")
|
|
|
|
has_access = false
|
|
|
|
-- permission is public = everybody has access, no need to check auth
|
|
|
|
elseif permission["public"] then
|
|
|
|
logger:debug("Someone tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
|
|
has_access = true
|
|
|
|
-- Check auth header, assume the route is protected
|
|
|
|
else
|
|
|
|
is_logged_in, authUser, authPasswordEnc = check_authentication()
|
2023-07-15 19:51:31 +02:00
|
|
|
|
2023-09-29 14:30:14 +02:00
|
|
|
-- Unauthenticated user, deny access
|
|
|
|
if authUser == nil then
|
|
|
|
logger:debug("Denied unauthenticated access to "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
2023-11-28 19:26:03 +01:00
|
|
|
has_access = false
|
2023-07-15 19:51:31 +02:00
|
|
|
else
|
2023-09-29 14:30:14 +02:00
|
|
|
logger:debug("User "..authUser.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
|
|
|
2023-11-28 19:26:03 +01:00
|
|
|
-- The user has permission to access the content if s.he is in the list of allowed users
|
|
|
|
if element_is_in_table(authUser, permission["users"]) then
|
|
|
|
logger:debug("User "..authUser.." can access "..ngx.var.host..ngx.var.uri..uri_args_string())
|
|
|
|
has_access = true
|
|
|
|
else
|
|
|
|
logger:debug("User "..authUser.." cannot access "..ngx.var.uri)
|
|
|
|
has_access = false
|
|
|
|
end
|
2023-07-15 19:51:31 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- ###########################################################################
|
|
|
|
-- 5. CLEAR USER-PROVIDED AUTH HEADER
|
2020-05-21 21:53:04 +02:00
|
|
|
--
|
2023-07-15 19:51:31 +02:00
|
|
|
-- Which could be spoofing attempts
|
2017-04-02 23:47:54 +02:00
|
|
|
--
|
2024-01-30 19:35:47 +01:00
|
|
|
-- Apps can opt out of the auth spoofing protection using the setting
|
|
|
|
-- 'protect_against_basic_auth_spoofing' set to false if they really need to,
|
|
|
|
-- but that's a huge security hole and ultimately should never be done...
|
|
|
|
--
|
2023-07-15 19:51:31 +02:00
|
|
|
-- ###########################################################################
|
|
|
|
|
|
|
|
if permission ~= nil and ngx.req.get_headers()["Authorization"] ~= nil then
|
2024-01-30 19:35:47 +01:00
|
|
|
if permission["protect_against_basic_auth_spoofing"] == false then
|
2023-09-02 19:36:18 +02:00
|
|
|
-- Ignore if not a Basic auth header
|
|
|
|
-- otherwise, we interpret this as a Auth header spoofing attempt and clear it
|
|
|
|
local auth_header_from_client = ngx.req.get_headers()["Authorization"]
|
|
|
|
_, _, b64_cred = string.find(auth_header_from_client, "^Basic%s+(.+)$")
|
|
|
|
if b64_cred ~= nil then
|
|
|
|
ngx.req.clear_header("Authorization")
|
|
|
|
end
|
2023-07-15 19:51:31 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- ###########################################################################
|
|
|
|
-- 6. EFFECTIVELY PASS OR DENY ACCESS
|
2017-04-02 23:47:54 +02:00
|
|
|
--
|
2023-07-15 19:51:31 +02:00
|
|
|
-- If the user has access (either because app is public OR logged in + authorized)
|
|
|
|
-- -> pass + possibly inject the Basic Auth header on the fly such that the app can know which user is logged in
|
2017-04-02 23:47:54 +02:00
|
|
|
--
|
2023-07-15 19:51:31 +02:00
|
|
|
-- Otherwise, the user can't access
|
|
|
|
-- -> either because not logged in at all, in that case, redirect to the portal WITH a callback url to redirect to after logging in
|
|
|
|
-- -> or because user is logged in, but has no access .. in that case just redirect to the portal
|
|
|
|
-- ###########################################################################
|
2017-04-02 23:47:54 +02:00
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
function set_basic_auth_header()
|
|
|
|
|
|
|
|
-- cf. https://en.wikipedia.org/wiki/Basic_access_authentication
|
2017-04-02 23:47:54 +02:00
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- authPasswordEnc is actually a string formatted as <password_enc_b64>|<iv_b64>
|
|
|
|
-- For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA==
|
|
|
|
-- The password is encoded using AES-256-CBC with the IV being the right-side data
|
|
|
|
-- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
|
2023-12-23 20:08:35 +01:00
|
|
|
|
|
|
|
-- Tmp, possibly permanent removal of the code that inject the password inside the auth header,
|
|
|
|
-- which should not be needed in the vast majority of cases where the app just trust the $remote_user info/header ...
|
|
|
|
|
2024-01-30 19:40:28 +01:00
|
|
|
-- By default, the password is not injected anymore, unless the app has the
|
|
|
|
-- "auth_header" setting defined with value "basic-with-password"
|
2024-01-30 20:24:08 +01:00
|
|
|
-- (by default we use '-' as a dummy value though, otherwise the header doesn't work as expected..)
|
|
|
|
local password = "-"
|
2024-01-30 19:40:28 +01:00
|
|
|
if permission["auth_header"] == "basic-with-password" then
|
|
|
|
local password_enc_b64, iv_b64 = authPasswordEnc:match("([^|]+)|([^|]+)")
|
|
|
|
local password_enc = ngx.decode_base64(password_enc_b64)
|
|
|
|
local iv = ngx.decode_base64(iv_b64)
|
2024-01-30 20:52:23 +01:00
|
|
|
password = cipher.new('aes-256-cbc'):decrypt(COOKIE_SECRET, iv):final(password_enc)
|
2024-01-30 19:40:28 +01:00
|
|
|
end
|
2023-07-15 19:51:31 +02:00
|
|
|
|
|
|
|
-- Set `Authorization` header to enable HTTP authentification
|
|
|
|
ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
|
2024-01-30 19:40:28 +01:00
|
|
|
authUser..":"..password
|
2023-07-15 19:51:31 +02:00
|
|
|
))
|
|
|
|
end
|
|
|
|
|
|
|
|
-- 1st case : client has access
|
|
|
|
if has_access then
|
2023-09-02 19:36:18 +02:00
|
|
|
-- If Basic Authorization header are enable for this permission,
|
|
|
|
-- check if the user is actually logged in...
|
|
|
|
if permission["auth_header"] then
|
|
|
|
if is_logged_in == nil then
|
|
|
|
-- Login check was not performed yet because the app is public
|
|
|
|
logger:debug("Checking authentication because the app requires auth_header")
|
|
|
|
is_logged_in, authUser, authPasswordEnc = check_authentication()
|
|
|
|
end
|
|
|
|
if is_logged_in then
|
|
|
|
-- add it to the response
|
2023-07-15 19:51:31 +02:00
|
|
|
set_basic_auth_header()
|
2020-04-01 00:43:59 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-07-15 19:51:31 +02:00
|
|
|
-- Pass
|
|
|
|
logger:debug("Allowing to pass through "..ngx.var.uri)
|
|
|
|
return
|
2013-10-20 17:24:44 +02:00
|
|
|
|
2020-09-20 18:00:37 +02:00
|
|
|
-- 2nd case : no access ... redirect to portal / login form
|
2020-05-21 21:51:55 +02:00
|
|
|
else
|
|
|
|
|
2023-08-13 18:17:52 +02:00
|
|
|
portal_domain = conf["domain_portal_urls"][ngx.var.host]
|
|
|
|
if portal_domain == nil then
|
2023-09-29 14:31:30 +02:00
|
|
|
logger:debug("Domain " .. ngx.var.host .. " is not configured for SSOWat, falling back to default")
|
|
|
|
portal_domain = conf["domain_portal_urls"]["default"]
|
|
|
|
if portal_domain ~= nil then
|
2023-11-28 19:26:03 +01:00
|
|
|
if string.starts(portal_domain, '/') then
|
|
|
|
portal_domain = ngx.var.host .. portal_domain
|
|
|
|
end
|
|
|
|
return ngx.redirect("https://" .. portal_domain)
|
|
|
|
end
|
2023-09-29 14:31:30 +02:00
|
|
|
end
|
|
|
|
if portal_domain == nil then
|
2023-11-28 19:26:03 +01:00
|
|
|
ngx.header['Content-Type'] = "text/html"
|
|
|
|
ngx.status = 400
|
|
|
|
ngx.say("Unmanaged domain")
|
|
|
|
return ngx.exit(200)
|
2023-08-13 18:17:52 +02:00
|
|
|
end
|
2023-09-29 14:31:30 +02:00
|
|
|
|
2023-08-13 18:17:52 +02:00
|
|
|
portal_url = "https://" .. portal_domain
|
|
|
|
logger:debug("Redirecting to portal : " .. portal_url)
|
|
|
|
|
|
|
|
if is_logged_in then
|
2023-11-28 19:59:13 +01:00
|
|
|
return ngx.redirect(portal_url.."?msg=access_denied")
|
2020-09-20 18:00:37 +02:00
|
|
|
else
|
2023-07-15 19:51:31 +02:00
|
|
|
local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. uri_args_string()
|
2023-07-15 21:22:27 +02:00
|
|
|
|
|
|
|
-- User ain't logged in, redirect to the portal where we expect the user to login,
|
|
|
|
-- then be redirected to the original URL by the portal, encoded as base64
|
|
|
|
--
|
|
|
|
-- NB. for security reason, the client/app handling the callback should check
|
|
|
|
-- that the back URL is legit, i.e it should be on the same domain (or a subdomain)
|
|
|
|
-- than the portal. Otherwise, a malicious actor could create a deceptive link
|
|
|
|
-- that would in fact redirect to a different domain, tricking the user that may
|
|
|
|
-- not realize this.
|
|
|
|
return ngx.redirect(portal_url.."?r="..ngx.encode_base64(back_url))
|
2020-09-20 18:00:37 +02:00
|
|
|
end
|
2020-05-21 21:51:55 +02:00
|
|
|
end
|