mirror of
https://github.com/YunoHost/SSOwat.git
synced 2024-09-03 20:06:27 +02:00
Moar epic refactoring ... merge 'helpers.lua' inside 'access.lua' to reduce complexity ...
This commit is contained in:
parent
df094ea0e3
commit
02952d0202
4 changed files with 263 additions and 272 deletions
275
access.lua
275
access.lua
|
@ -2,64 +2,153 @@
|
||||||
-- access.lua
|
-- access.lua
|
||||||
--
|
--
|
||||||
-- This file is executed at every request on a protected domain or server.
|
-- 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.
|
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Import helpers
|
|
||||||
local hlp = require("helpers")
|
|
||||||
|
|
||||||
-- Initialize and get configuration
|
|
||||||
hlp.refresh_config()
|
|
||||||
local conf = hlp.get_config()
|
|
||||||
|
|
||||||
-- Just a note for the client to know that he passed through the SSO
|
-- Just a note for the client to know that he passed through the SSO
|
||||||
ngx.header["X-SSO-WAT"] = "You've just been SSOed"
|
ngx.header["X-SSO-WAT"] = "You've just been SSOed"
|
||||||
|
|
||||||
local is_logged_in = hlp.check_authentication()
|
-- Misc imports
|
||||||
|
local jwt = require("vendor.luajwtjitsi.luajwtjitsi")
|
||||||
|
local cipher = require('openssl.cipher')
|
||||||
|
local rex = require("rex_pcre")
|
||||||
|
|
||||||
--
|
-- ###########################################################################
|
||||||
-- 3. REDIRECTED URLS
|
-- 0. Misc helpers because Lua has no sugar ...
|
||||||
--
|
-- ###########################################################################
|
||||||
-- 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)
|
-- Get configuration (we do this here, the conf is re-read every time unless
|
||||||
if hlp.string.starts(redirect_url, "http://")
|
-- the file's timestamp didnt change)
|
||||||
or hlp.string.starts(redirect_url, "https://") then
|
local config = require("config")
|
||||||
return hlp.redirect(redirect_url)
|
local conf = config.get_config()
|
||||||
elseif hlp.string.starts(redirect_url, "/") then
|
|
||||||
return hlp.redirect(ngx.var.scheme.."://"..ngx.var.host..redirect_url)
|
-- The 'match' function uses PCRE regex as default
|
||||||
|
-- If '%.' is found in the regex, we assume it's a LUA regex (legacy code)
|
||||||
|
-- 'match' returns the matched text.
|
||||||
|
function match(s, regex)
|
||||||
|
if not string.find(regex, '%%%.') then
|
||||||
|
return rex.match(s, regex)
|
||||||
else
|
else
|
||||||
return hlp.redirect(ngx.var.scheme.."://"..redirect_url)
|
return string.match(s,regex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test whether a string starts with another
|
||||||
|
function string.starts(String, Start)
|
||||||
|
if not String then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return string.sub(String, 1, string.len(Start)) == Start
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- ###########################################################################
|
||||||
|
-- 1. AUTHENTICATION
|
||||||
|
-- Check wether or not this is a logged-in user
|
||||||
|
-- ###########################################################################
|
||||||
|
|
||||||
|
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"]]
|
||||||
|
|
||||||
|
decoded, err = jwt.verify(cookie, "HS256", cookie_secret)
|
||||||
|
|
||||||
|
-- FIXME : we might want also a way to identify expired/invalidated cookies,
|
||||||
|
-- e.g. a user that got deleted after being logged in ...
|
||||||
|
|
||||||
|
if err ~= nil then
|
||||||
|
return false, nil, nil
|
||||||
|
else
|
||||||
|
return true, decoded["user"], decoded["pwd"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_logged_in, authUser, authPasswordEnc = check_authentication()
|
||||||
|
|
||||||
|
-- ###########################################################################
|
||||||
|
-- 2. REDIRECTED URLS
|
||||||
|
-- If the URL matches one of the `redirected_urls` in the configuration file,
|
||||||
|
-- just redirect to the target URL/URI
|
||||||
|
-- ###########################################################################
|
||||||
|
|
||||||
|
function redirect(url)
|
||||||
|
logger:debug("Redirecting to "..url)
|
||||||
|
-- For security reason we don't allow to redirect onto unknown domain
|
||||||
|
-- And if `uri_args.r` contains line break, someone is probably trying to
|
||||||
|
-- pass some additional headers
|
||||||
|
|
||||||
|
-- This should cover the following cases:
|
||||||
|
-- https://malicious.domain.tld/foo/bar
|
||||||
|
-- http://malicious.domain.tld/foo/bar
|
||||||
|
-- https://malicious.domain.tld:1234/foo
|
||||||
|
-- malicious.domain.tld/foo/bar
|
||||||
|
-- (/foo/bar, in which case no need to make sure it's prefixed with https://)
|
||||||
|
if not string.starts(url, "/") and not string.starts(url, "http://") and not string.starts(url, "https://") then
|
||||||
|
url = "https://"..url
|
||||||
|
end
|
||||||
|
local is_known_domain = string.starts(url, "/")
|
||||||
|
for _, domain in ipairs(conf["domains"]) do
|
||||||
|
if is_known_domain then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Replace - character to %- because - is a special char for regex in lua
|
||||||
|
domain = string.gsub(domain, "%-","%%-")
|
||||||
|
is_known_domain = is_known_domain or url:match("^https?://"..domain.."/?") ~= nil
|
||||||
|
end
|
||||||
|
if string.match(url, "(.*)\n") or not is_known_domain then
|
||||||
|
logger:debug("Unauthorized redirection to "..url)
|
||||||
|
url = conf.portal_url
|
||||||
|
end
|
||||||
|
return ngx.redirect(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
else
|
||||||
|
return ngx.var.scheme.."://"..redirect_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if conf["redirected_urls"] then
|
if conf["redirected_urls"] then
|
||||||
for url, redirect_url in pairs(conf["redirected_urls"]) do
|
for url, redirect_url in pairs(conf["redirected_urls"]) do
|
||||||
if url == ngx.var.host..ngx.var.uri..hlp.uri_args_string()
|
if url == ngx.var.host..ngx.var.uri..uri_args_string()
|
||||||
or url == ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..hlp.uri_args_string()
|
or url == ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..uri_args_string()
|
||||||
or url == ngx.var.uri..hlp.uri_args_string() then
|
or url == ngx.var.uri..uri_args_string() then
|
||||||
hlp.logger:debug("Requested URI is in redirected_urls")
|
logger:debug("Requested URI is in redirected_urls")
|
||||||
detect_redirection(redirect_url)
|
redirect(convert_to_absolute_url(redirect_url))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if conf["redirected_regex"] then
|
if conf["redirected_regex"] then
|
||||||
for regex, redirect_url in pairs(conf["redirected_regex"]) do
|
for regex, redirect_url in pairs(conf["redirected_regex"]) do
|
||||||
if hlp.match(ngx.var.host..ngx.var.uri..hlp.uri_args_string(), regex)
|
if match(ngx.var.host..ngx.var.uri..uri_args_string(), regex)
|
||||||
or hlp.match(ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..hlp.uri_args_string(), regex)
|
or match(ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri..uri_args_string(), regex)
|
||||||
or hlp.match(ngx.var.uri..hlp.uri_args_string(), regex) then
|
or match(ngx.var.uri..uri_args_string(), regex) then
|
||||||
hlp.logger:debug("Requested URI is in redirected_regex")
|
logger:debug("Requested URI is in redirected_regex")
|
||||||
detect_redirection(redirect_url)
|
redirect(convert_to_absolute_url(redirect_url))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--
|
-- ###########################################################################
|
||||||
-- 4. IDENTIFY THE RELEVANT PERMISSION
|
-- 3. IDENTIFY PERMISSION MATCHING THE REQUESTED URL
|
||||||
--
|
--
|
||||||
-- In particular, the conf is filled with permissions such as:
|
-- In particular, the conf is filled with permissions such as:
|
||||||
--
|
--
|
||||||
|
@ -78,7 +167,7 @@ end
|
||||||
--
|
--
|
||||||
-- And we find the best matching permission by trying to match the request uri
|
-- 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.
|
-- against all the uris rules/regexes from the conf and keep the longest matching one.
|
||||||
--
|
-- ###########################################################################
|
||||||
|
|
||||||
permission = nil
|
permission = nil
|
||||||
longest_url_match = ""
|
longest_url_match = ""
|
||||||
|
@ -96,7 +185,7 @@ for permission_name, permission_infos in pairs(conf["permissions"]) do
|
||||||
url = "^"..url
|
url = "^"..url
|
||||||
end
|
end
|
||||||
|
|
||||||
local m = hlp.match(ngx_full_url, url)
|
local m = match(ngx_full_url, url)
|
||||||
if m ~= nil and string.len(m) > string.len(longest_url_match) then
|
if m ~= nil and string.len(m) > string.len(longest_url_match) then
|
||||||
longest_url_match = m
|
longest_url_match = m
|
||||||
permission = permission_infos
|
permission = permission_infos
|
||||||
|
@ -106,33 +195,129 @@ for permission_name, permission_infos in pairs(conf["permissions"]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ###########################################################################
|
||||||
|
-- 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
|
||||||
|
-- ###########################################################################
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Check whether a user is allowed to access a URL using the `permissions` directive
|
||||||
|
-- of the configuration file
|
||||||
|
function check_has_access(permission)
|
||||||
|
|
||||||
|
if permission == nil then
|
||||||
|
logger:debug("No permission matching request for "..ngx.var.uri)
|
||||||
|
return false
|
||||||
|
|
||||||
|
-- Public access
|
||||||
|
if authUser == nil or permission["public"] then
|
||||||
|
user = authUser or "A visitor"
|
||||||
|
logger:debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
||||||
|
return permission["public"]
|
||||||
|
end
|
||||||
|
|
||||||
|
logger:debug("User "..authUser.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
||||||
|
|
||||||
|
-- The user has permission to access the content if 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())
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
logger:debug("User "..authUser.." cannot access "..ngx.var.uri)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
has_access = check_has_access(permission)
|
||||||
|
|
||||||
|
|
||||||
|
-- ###########################################################################
|
||||||
|
-- 5. CLEAR USER-PROVIDED AUTH HEADER
|
||||||
--
|
--
|
||||||
|
-- Which could be spoofing attempts
|
||||||
|
-- Unfortunately we can't yolo-clear them on every route because some
|
||||||
|
-- apps use legit basic auth mechanism ...
|
||||||
--
|
--
|
||||||
-- 5. APPLY PERMISSION
|
-- "Remote user" refers to the fact that Basic Auth headers is coupled to
|
||||||
|
-- the $remote_user var in nginx, typically used by PHP apps
|
||||||
|
-- ###########################################################################
|
||||||
|
|
||||||
|
if permission ~= nil and ngx.req.get_headers()["Authorization"] ~= nil then
|
||||||
|
perm_user_remote_user_var_in_nginx_conf = permission["use_remote_user_var_in_nginx_conf"]
|
||||||
|
if perm_user_remote_user_var_in_nginx_conf == nil or perm_user_remote_user_var_in_nginx_conf == true then
|
||||||
|
-- Ignore if not a Basic auth header
|
||||||
|
-- otherwise, we interpret this as a Auth header spoofing attempt and clear it
|
||||||
|
_, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$")
|
||||||
|
if b64_cred ~= nil then
|
||||||
|
ngx.req.clear_header("Authorization")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ###########################################################################
|
||||||
|
-- 6. EFFECTIVELY PASS OR DENY ACCESS
|
||||||
--
|
--
|
||||||
|
-- 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
|
||||||
--
|
--
|
||||||
|
-- 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
|
||||||
|
-- ###########################################################################
|
||||||
|
|
||||||
|
function set_basic_auth_header()
|
||||||
|
|
||||||
|
-- cf. https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
local password_enc_b64, iv_b64 = authPasswordEnc:match("([^|]+)|([^|]+)")
|
||||||
|
local password_enc = ngx.decode_base64(password_enc_b64)
|
||||||
|
local iv = ngx.decode_base64(iv_b64)
|
||||||
|
local password = cipher.new('aes-256-cbc'):decrypt(cookie_secret, iv):final(password_enc)
|
||||||
|
|
||||||
|
-- Set `Authorization` header to enable HTTP authentification
|
||||||
|
ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
|
||||||
|
authUser..":"..password
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
-- 1st case : client has access
|
-- 1st case : client has access
|
||||||
|
if has_access then
|
||||||
if hlp.has_access(permission) then
|
|
||||||
|
|
||||||
if is_logged_in then
|
if is_logged_in then
|
||||||
-- If Basic Authorization header are enable for this permission,
|
-- If Basic Authorization header are enable for this permission,
|
||||||
-- add it to the response
|
-- add it to the response
|
||||||
if permission["auth_header"] then
|
if permission["auth_header"] then
|
||||||
hlp.set_basic_auth_header()
|
set_basic_auth_header()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return hlp.pass()
|
-- Pass
|
||||||
|
logger:debug("Allowing to pass through "..ngx.var.uri)
|
||||||
|
return
|
||||||
|
|
||||||
-- 2nd case : no access ... redirect to portal / login form
|
-- 2nd case : no access ... redirect to portal / login form
|
||||||
else
|
else
|
||||||
|
|
||||||
if is_logged_in then
|
if is_logged_in then
|
||||||
return hlp.redirect(conf.portal_url)
|
return redirect(conf.portal_url)
|
||||||
else
|
else
|
||||||
local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string()
|
local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. uri_args_string()
|
||||||
return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
|
return redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,9 @@ local config_persistent_attributes = nil
|
||||||
|
|
||||||
local conf = {}
|
local conf = {}
|
||||||
|
|
||||||
|
local conf_path = "/etc/ssowat/conf.json"
|
||||||
|
|
||||||
|
|
||||||
function get_cookie_secret()
|
function get_cookie_secret()
|
||||||
|
|
||||||
local conf_file = assert(io.open(conf_path, "r"), "Configuration file is missing")
|
local conf_file = assert(io.open(conf_path, "r"), "Configuration file is missing")
|
||||||
|
|
218
helpers.lua
218
helpers.lua
|
@ -1,218 +0,0 @@
|
||||||
--
|
|
||||||
-- helpers.lua
|
|
||||||
--
|
|
||||||
-- This is a file called at every request by the `access.lua` file. It contains
|
|
||||||
-- a set of useful functions related to HTTP and LDAP.
|
|
||||||
--
|
|
||||||
|
|
||||||
module('helpers', package.seeall)
|
|
||||||
|
|
||||||
local conf = config.get_config()
|
|
||||||
local Logging = require("logging")
|
|
||||||
local jwt = require("vendor.luajwtjitsi.luajwtjitsi")
|
|
||||||
local cipher = require('openssl.cipher')
|
|
||||||
local mime = require("mime")
|
|
||||||
local rex = require("rex_pcre")
|
|
||||||
|
|
||||||
local appender = function(self, level, message)
|
|
||||||
|
|
||||||
-- Output to log file
|
|
||||||
local fp = io.open(log_file, "a")
|
|
||||||
local str = string.format("[%-6s%s] %s\n", level:upper(), os.date(), message)
|
|
||||||
fp:write(str)
|
|
||||||
fp:close()
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local logger = Logging.new(appender)
|
|
||||||
--logger:setLevel(logger.DEBUG) -- FIXME
|
|
||||||
|
|
||||||
local is_logged_in = false
|
|
||||||
|
|
||||||
function refresh_config()
|
|
||||||
conf = config.get_config()
|
|
||||||
end
|
|
||||||
|
|
||||||
function get_config()
|
|
||||||
return conf
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
-- The 'match' function uses PCRE regex as default
|
|
||||||
-- If '%.' is found in the regex, we assume it's a LUA regex (legacy code)
|
|
||||||
-- 'match' returns the matched text.
|
|
||||||
function match(s, regex)
|
|
||||||
if not string.find(regex, '%%%.') then
|
|
||||||
return rex.match(s, regex)
|
|
||||||
else
|
|
||||||
return string.match(s,regex)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test whether a string starts with another
|
|
||||||
function string.starts(String, Start)
|
|
||||||
if not String then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return string.sub(String, 1, string.len(Start)) == Start
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Test whether a string ends with another
|
|
||||||
function string.ends(String, End)
|
|
||||||
return End=='' or string.sub(String, -string.len(End)) == End
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
|
|
||||||
-- Validate authentification
|
|
||||||
--
|
|
||||||
-- Check if the session cookies are set, and rehash server + client information
|
|
||||||
-- to match the session hash.
|
|
||||||
--
|
|
||||||
function check_authentication()
|
|
||||||
|
|
||||||
local token = ngx.var["cookie_" .. conf["cookie_name"]]
|
|
||||||
|
|
||||||
decoded, err = jwt.verify(token, "HS256", cookie_secret)
|
|
||||||
|
|
||||||
if err ~= nil then
|
|
||||||
-- FIXME : log an authentication error to be caught by fail2ban ? or should it happen somewhere else ? (check the old code)
|
|
||||||
authUser = nil
|
|
||||||
authPasswordEnc = nil
|
|
||||||
is_logged_in = false
|
|
||||||
return is_logged_in
|
|
||||||
end
|
|
||||||
|
|
||||||
-- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
|
|
||||||
authUser = decoded["user"]
|
|
||||||
authPasswordEnc = decoded["pwd"]
|
|
||||||
is_logged_in = true
|
|
||||||
|
|
||||||
-- Gotta update authUser and is_logged_in
|
|
||||||
return is_logged_in
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extract the user password from cookie,
|
|
||||||
-- needed to create the basic auth header
|
|
||||||
function decrypt_user_password()
|
|
||||||
-- 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
|
|
||||||
local password_enc_b64, iv_b64 = authPasswordEnc:match("([^|]+)|([^|]+)")
|
|
||||||
local password_enc = mime.unb64(password_enc_b64)
|
|
||||||
local iv = mime.unb64(iv_b64)
|
|
||||||
return cipher.new('aes-256-cbc'):decrypt(cookie_secret, iv):final(password_enc)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check whether a user is allowed to access a URL using the `permissions` directive
|
|
||||||
-- of the configuration file
|
|
||||||
function has_access(permission, user)
|
|
||||||
user = user or authUser
|
|
||||||
|
|
||||||
if permission == nil then
|
|
||||||
logger:debug("No permission matching request for "..ngx.var.uri)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Public access
|
|
||||||
if user == nil or permission["public"] then
|
|
||||||
user = user or "A visitor"
|
|
||||||
logger:debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
||||||
return permission["public"]
|
|
||||||
end
|
|
||||||
|
|
||||||
logger:debug("User "..user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
||||||
|
|
||||||
-- The user has permission to access the content if he is in the list of allowed users
|
|
||||||
if element_is_in_table(user, permission["users"]) then
|
|
||||||
logger:debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string())
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
logger:debug("User "..user.." cannot access "..ngx.var.uri)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Set the authentication headers in order to pass credentials to the
|
|
||||||
-- application underneath.
|
|
||||||
function set_basic_auth_header(user)
|
|
||||||
local user = user or authUser
|
|
||||||
-- Set `Authorization` header to enable HTTP authentification
|
|
||||||
ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
|
|
||||||
user..":"..decrypt_user_password()
|
|
||||||
))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Set cookie and redirect (needed to properly set cookie)
|
|
||||||
function redirect(url)
|
|
||||||
logger:debug("Redirecting to "..url)
|
|
||||||
-- For security reason we don't allow to redirect onto unknown domain
|
|
||||||
-- And if `uri_args.r` contains line break, someone is probably trying to
|
|
||||||
-- pass some additional headers
|
|
||||||
|
|
||||||
-- This should cover the following cases:
|
|
||||||
-- https://malicious.domain.tld/foo/bar
|
|
||||||
-- http://malicious.domain.tld/foo/bar
|
|
||||||
-- https://malicious.domain.tld:1234/foo
|
|
||||||
-- malicious.domain.tld/foo/bar
|
|
||||||
-- (/foo/bar, in which case no need to make sure it's prefixed with https://)
|
|
||||||
if not string.starts(url, "/") and not string.starts(url, "http://") and not string.starts(url, "https://") then
|
|
||||||
url = "https://"..url
|
|
||||||
end
|
|
||||||
local is_known_domain = string.starts(url, "/")
|
|
||||||
for _, domain in ipairs(conf["domains"]) do
|
|
||||||
if is_known_domain then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
-- Replace - character to %- because - is a special char for regex in lua
|
|
||||||
domain = string.gsub(domain, "%-","%%-")
|
|
||||||
is_known_domain = is_known_domain or url:match("^https?://"..domain.."/?") ~= nil
|
|
||||||
end
|
|
||||||
if string.match(url, "(.*)\n") or not is_known_domain then
|
|
||||||
logger:debug("Unauthorized redirection to "..url)
|
|
||||||
url = conf.portal_url
|
|
||||||
end
|
|
||||||
return ngx.redirect(url)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Set cookie and go on with the response (needed to properly set cookie)
|
|
||||||
function pass()
|
|
||||||
logger:debug("Allowing to pass through "..ngx.var.uri)
|
|
||||||
|
|
||||||
-- When we are in the SSOwat portal, we need a default `content-type`
|
|
||||||
if string.ends(ngx.var.uri, "/")
|
|
||||||
or string.ends(ngx.var.uri, ".html")
|
|
||||||
or string.ends(ngx.var.uri, ".htm")
|
|
||||||
then
|
|
||||||
ngx.header["Content-Type"] = "text/html"
|
|
||||||
end
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
39
init.lua
39
init.lua
|
@ -3,31 +3,52 @@
|
||||||
--
|
--
|
||||||
-- This is the initialization file of SSOwat. It is called once at the Nginx
|
-- This is the initialization file of SSOwat. It is called once at the Nginx
|
||||||
-- server's start.
|
-- server's start.
|
||||||
-- Consequently, all the variables declared (along with libraries and
|
-- Consequently, all the variables declared (along with libraries and
|
||||||
-- translations) in this file will be *persistent* from one HTTP request to
|
-- translations) in this file will be *persistent* from one HTTP request to
|
||||||
-- another.
|
-- another.
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Path of the configuration
|
|
||||||
conf_path = "/etc/ssowat/conf.json"
|
|
||||||
log_file = "/var/log/nginx/ssowat.log"
|
|
||||||
|
|
||||||
-- Remove prepending '@' & trailing 'init.lua'
|
-- Remove prepending '@' & trailing 'init.lua'
|
||||||
script_path = string.sub(debug.getinfo(1).source, 2, -9)
|
script_path = string.sub(debug.getinfo(1).source, 2, -9)
|
||||||
|
|
||||||
-- Include local libs in package.path
|
-- Include local libs in package.path
|
||||||
package.path = package.path .. ";"..script_path.."?.lua"
|
package.path = package.path .. ";"..script_path.."?.lua"
|
||||||
|
|
||||||
-- Load libraries
|
|
||||||
local config = require("config")
|
|
||||||
|
|
||||||
-- Load cookie secret
|
-- Load cookie secret
|
||||||
|
-- IMPORTANT (though to be confirmed?)
|
||||||
|
-- in this context, the code is ran as root therefore we don't have to
|
||||||
|
-- add www-data in the file permissions, which could otherwise lead
|
||||||
|
-- to comprised apps running with the www-data group to read the secret file?
|
||||||
|
local config = require("config")
|
||||||
cookie_secret = config.get_cookie_secret()
|
cookie_secret = config.get_cookie_secret()
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Init logger
|
||||||
|
--
|
||||||
|
|
||||||
|
local log_file = "/var/log/nginx/ssowat.log"
|
||||||
|
|
||||||
-- Make sure the log file exists and we can write in it
|
-- Make sure the log file exists and we can write in it
|
||||||
io.popen("touch "..log_file)
|
io.popen("touch "..log_file)
|
||||||
io.popen("chown www-data "..log_file)
|
io.popen("chown www-data "..log_file)
|
||||||
io.popen("chmod u+w "..log_file)
|
io.popen("chmod u+w "..log_file)
|
||||||
|
|
||||||
|
local Logging = require("logging")
|
||||||
|
local appender = function(self, level, message)
|
||||||
|
|
||||||
|
-- Output to log file
|
||||||
|
local fp = io.open(log_file, "a")
|
||||||
|
local str = string.format("[%-6s%s] %s\n", level:upper(), os.date(), message)
|
||||||
|
fp:write(str)
|
||||||
|
fp:close()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
logger = Logging.new(appender)
|
||||||
|
|
||||||
|
-- FIXME : how to set logging level ?
|
||||||
|
--logger:setLevel(logger.DEBUG) -- FIXME
|
||||||
|
|
||||||
|
|
||||||
-- You should see that in your Nginx error logs by default
|
-- You should see that in your Nginx error logs by default
|
||||||
ngx.log(ngx.INFO, "SSOwat ready")
|
ngx.log(ngx.INFO, "SSOwat ready")
|
||||||
|
|
Loading…
Add table
Reference in a new issue