-- -- 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))