From 5fde1663f5ae6bfef8140557c395606c78e090ab Mon Sep 17 00:00:00 2001
From: Yves G
Date: Mon, 9 Aug 2021 17:43:42 +0200
Subject: [PATCH 1/5] minimum: login, portal, redirects; todo: refactor,
quality, security review
---
.editorconfig | 16 +
.gitignore | 1 +
Makefile | 105 ++++++
doc/samples/global.json | 8 +
doc/samples/login/login.css | 32 ++
doc/samples/login/login.html | 14 +
doc/samples/login/login.js | 1 +
doc/samples/portal/portal.css | 50 +++
doc/samples/portal/portal.html | 9 +
doc/samples/portal/portal.js | 1 +
doc/samples/test_pages/private_restricted.php | 2 +
.../test_pages/private_unrestricted.php | 2 +
doc/samples/test_pages/private_with_ban.php | 2 +
doc/samples/test_pages/public_access.php | 2 +
.../test_pages/sites/private_restricted.json | 23 ++
.../sites/private_unrestricted.json | 28 ++
.../test_pages/sites/private_with_ban.json | 31 ++
.../test_pages/sites/public_access.json | 20 ++
src/do_access.lua | 51 +++
src/do_init.lua | 28 ++
src/ssso_auth.lua | 39 +++
src/ssso_base64.lua | 19 ++
src/ssso_config.lua | 38 +++
src/ssso_crypto.lua | 133 ++++++++
src/ssso_log.lua | 14 +
src/ssso_login.lua | 104 ++++++
src/ssso_nginx.lua | 156 +++++++++
src/ssso_oauth2.lua | 14 +
src/ssso_portal.lua | 55 +++
src/ssso_profile.lua | 63 ++++
src/ssso_sessions.lua | 24 ++
src/ssso_sites.lua | 215 ++++++++++++
src/ssso_util.lua | 18 +
test/aes.utest.lua | 23 ++
test/alt/bit.lua | 1 +
test/alt/ngx.lua | 135 ++++++++
test/alt/ngx/base64.lua | 18 +
test/alt/resty/openssl/cipher.lua | 35 ++
test/alt/resty/random.lua | 9 +
test/alt/resty/sha256.lua | 20 ++
test/anonymous1.ctest.lua | 18 +
test/anonymous2.ctest.lua | 18 +
test/anonymous3.ctest.lua | 18 +
test/anonymous4.ctest.lua | 18 +
test/anonymous5.ctest.lua | 18 +
test/auth.utest.lua | 19 ++
test/config.utest.lua | 16 +
test/crypto.utest.lua | 50 +++
test/global.json | 8 +
test/login.utest.lua | 203 ++++++++++++
test/login/login.css | 1 +
test/login/login.html | 7 +
test/login/login.js | 1 +
test/login1.ctest.lua | 28 ++
test/login2.ctest.lua | 21 ++
test/login3.ctest.lua | 21 ++
test/login4.ctest.lua | 20 ++
test/login5.ctest.lua | 23 ++
test/login6.ctest.lua | 32 ++
test/login7.ctest.lua | 22 ++
test/nginx.utest.lua | 313 ++++++++++++++++++
test/portal/portal.css | 1 +
test/portal/portal.html | 7 +
test/portal/portal.js | 1 +
test/portal1.ctest.lua | 37 +++
test/portal2.ctest.lua | 25 ++
test/portal3.ctest.lua | 25 ++
test/portal4.ctest.lua | 22 ++
test/portal5.ctest.lua | 19 ++
test/profile.utest.lua | 81 +++++
test/random.utest.lua | 12 +
test/sessions.utest.lua | 61 ++++
test/sha256.utest.lua | 13 +
test/sites.utest.lua | 289 ++++++++++++++++
test/sites/mixed.json | 56 ++++
test/sites/private.json | 25 ++
test/sites/public.json | 31 ++
test/util.utest.lua | 14 +
78 files changed, 3153 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitignore
create mode 100644 Makefile
create mode 100644 doc/samples/global.json
create mode 100644 doc/samples/login/login.css
create mode 100644 doc/samples/login/login.html
create mode 100644 doc/samples/login/login.js
create mode 100644 doc/samples/portal/portal.css
create mode 100644 doc/samples/portal/portal.html
create mode 100644 doc/samples/portal/portal.js
create mode 100644 doc/samples/test_pages/private_restricted.php
create mode 100644 doc/samples/test_pages/private_unrestricted.php
create mode 100644 doc/samples/test_pages/private_with_ban.php
create mode 100644 doc/samples/test_pages/public_access.php
create mode 100644 doc/samples/test_pages/sites/private_restricted.json
create mode 100644 doc/samples/test_pages/sites/private_unrestricted.json
create mode 100644 doc/samples/test_pages/sites/private_with_ban.json
create mode 100644 doc/samples/test_pages/sites/public_access.json
create mode 100644 src/do_access.lua
create mode 100644 src/do_init.lua
create mode 100644 src/ssso_auth.lua
create mode 100644 src/ssso_base64.lua
create mode 100644 src/ssso_config.lua
create mode 100644 src/ssso_crypto.lua
create mode 100644 src/ssso_log.lua
create mode 100644 src/ssso_login.lua
create mode 100644 src/ssso_nginx.lua
create mode 100644 src/ssso_oauth2.lua
create mode 100644 src/ssso_portal.lua
create mode 100644 src/ssso_profile.lua
create mode 100644 src/ssso_sessions.lua
create mode 100644 src/ssso_sites.lua
create mode 100644 src/ssso_util.lua
create mode 100644 test/aes.utest.lua
create mode 100644 test/alt/bit.lua
create mode 100644 test/alt/ngx.lua
create mode 100644 test/alt/ngx/base64.lua
create mode 100644 test/alt/resty/openssl/cipher.lua
create mode 100644 test/alt/resty/random.lua
create mode 100644 test/alt/resty/sha256.lua
create mode 100644 test/anonymous1.ctest.lua
create mode 100644 test/anonymous2.ctest.lua
create mode 100644 test/anonymous3.ctest.lua
create mode 100644 test/anonymous4.ctest.lua
create mode 100644 test/anonymous5.ctest.lua
create mode 100644 test/auth.utest.lua
create mode 100644 test/config.utest.lua
create mode 100644 test/crypto.utest.lua
create mode 100644 test/global.json
create mode 100644 test/login.utest.lua
create mode 100644 test/login/login.css
create mode 100644 test/login/login.html
create mode 100644 test/login/login.js
create mode 100644 test/login1.ctest.lua
create mode 100644 test/login2.ctest.lua
create mode 100644 test/login3.ctest.lua
create mode 100644 test/login4.ctest.lua
create mode 100644 test/login5.ctest.lua
create mode 100644 test/login6.ctest.lua
create mode 100644 test/login7.ctest.lua
create mode 100644 test/nginx.utest.lua
create mode 100644 test/portal/portal.css
create mode 100644 test/portal/portal.html
create mode 100644 test/portal/portal.js
create mode 100644 test/portal1.ctest.lua
create mode 100644 test/portal2.ctest.lua
create mode 100644 test/portal3.ctest.lua
create mode 100644 test/portal4.ctest.lua
create mode 100644 test/portal5.ctest.lua
create mode 100644 test/profile.utest.lua
create mode 100644 test/random.utest.lua
create mode 100644 test/sessions.utest.lua
create mode 100644 test/sha256.utest.lua
create mode 100644 test/sites.utest.lua
create mode 100644 test/sites/mixed.json
create mode 100644 test/sites/private.json
create mode 100644 test/sites/public.json
create mode 100644 test/util.utest.lua
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..b860e67
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[Makefile]
+indent_style = tab
+tab_width = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c24bd96
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,105 @@
+# https://stackoverflow.com/a/23324703
+ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
+
+lua_family = 5.1
+lua_version = ${lua_family}.5
+lua_suffix =
+lua_build_platform = linux
+luarocks_version = 3.7.0
+
+lua_src = http://www.lua.org/ftp/lua-${lua_version}.tar.gz
+luarocks_src = http://luarocks.github.io/luarocks/releases/luarocks-${luarocks_version}.tar.gz
+
+lua_root = ${ROOT_DIR}/target/dist
+lua_mods = ${lua_root}/share/lua/${lua_family}
+lua_cmods = ${lua_root}/lib/lua/${lua_family}
+
+run_test_file = env \
+ LUA_PATH="${ROOT_DIR}/test/alt/?.lua;${lua_mods}/?.lua;${ROOT_DIR}/target/dist/etc/nginx/ssso/?.lua" \
+ LUA_CPATH="${lua_cmods}/?.so" \
+ ${lua_root}/bin/lua${lua_suffix}
+
+all: test
+
+test: test-env
+ ${run_test_file} ${ROOT_DIR}/test/aes.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/random.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/sha256.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/util.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/config.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/auth.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/nginx.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/profile.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/sites.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/crypto.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/sessions.utest.lua
+ ${run_test_file} ${ROOT_DIR}/test/anonymous1.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/anonymous2.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/anonymous3.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/anonymous4.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/anonymous5.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login1.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login2.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login3.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login4.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login5.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login6.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/login7.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/portal1.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/portal2.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/portal3.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/portal4.ctest.lua
+ ${run_test_file} ${ROOT_DIR}/test/portal5.ctest.lua
+
+test-env: run-env target/dist/etc/nginx/ssso ${lua_cmods}/bit32.so ${lua_cmods}/cjson.so ${lua_mods}/resty/easy-crypto.lua ${lua_mods}/luaunit.lua
+
+target/dist/etc/nginx/ssso: src test/global.json test/login test/portal test/sites
+ rm -rf target/dist/etc/nginx/ssso; \
+ mkdir -p target/dist/etc/nginx; \
+ cp -rs ${ROOT_DIR}/src target/dist/etc/nginx/ssso; \
+ cp -rs ${ROOT_DIR}/test/{global.json,login,portal,sites} target/dist/etc/nginx/ssso/
+
+run-env: ${lua_root}/bin/lua${lua_suffix}
+
+${lua_root}/bin/lua${lua_suffix}: target/src/lua/lua-${lua_version}.tar.gz
+ ( \
+ cd target/src/lua; \
+ rm -rf lua-${lua_version}; \
+ tar -xzf lua-${lua_version}.tar.gz; \
+ cd lua-${lua_version}; \
+ make ${lua_build_platform} test install INSTALL_TOP="${lua_root}" \
+ )
+
+${lua_root}/bin/luarocks: target/src/luarocks/luarocks-${luarocks_version}.tar.gz
+ ( \
+ cd target/src/luarocks; \
+ rm -rf luarocks-${luarocks_version}; \
+ tar -xzf luarocks-${luarocks_version}.tar.gz; \
+ cd luarocks-${luarocks_version}; \
+ ./configure --prefix="${lua_root}" --with-lua="${lua_root}" --lua-suffix=${lua_suffix}; \
+ make; \
+ make install \
+ )
+
+${lua_cmods}/bit32.so: ${lua_root}/bin/luarocks
+ ${lua_root}/bin/luarocks install bit32
+
+${lua_cmods}/cjson.so: ${lua_root}/bin/luarocks
+ ${lua_root}/bin/luarocks install lua-cjson
+
+${lua_mods}/resty/easy-crypto.lua: ${lua_root}/bin/luarocks
+ ${lua_root}/bin/luarocks install lua-easy-crypto
+
+${lua_mods}/luaunit.lua: ${lua_root}/bin/luarocks
+ ${lua_root}/bin/luarocks install luaunit
+
+target/src/lua/lua-${lua_version}.tar.gz:
+ mkdir -p target/src/lua; \
+ curl -so target/src/lua/lua-${lua_version}.tar.gz "${lua_src}"
+
+target/src/luarocks/luarocks-${luarocks_version}.tar.gz:
+ mkdir -p target/src/luarocks; \
+ curl -so target/src/luarocks/luarocks-${luarocks_version}.tar.gz "${luarocks_src}"
+
+.PHONY: all run-env test-env test
diff --git a/doc/samples/global.json b/doc/samples/global.json
new file mode 100644
index 0000000..8da972f
--- /dev/null
+++ b/doc/samples/global.json
@@ -0,0 +1,8 @@
+{
+ "auth": {
+ "check": "/usr/bin/ldapsearch -x -D \"uid=\ru.,ou=users,dc=example,dc=org\" -w \"\rp.\" -b 'ou=users,dc=example,dc=org' -s one -LLL -l 1 -z 1 \"(uid=\ru.)\" cn mail | /usr/bin/gawk '/^cn/{n=gensub(/cn: */,\"\",1)};/^mail/{m=gensub(/mail: */,\"\",1)};END{printf(\"%s\\n%s\\n\",n,m)}'"
+ },
+ "session_seconds": 3600,
+ "sso_host": "example.org",
+ "sso_prefix": "/ssso"
+}
diff --git a/doc/samples/login/login.css b/doc/samples/login/login.css
new file mode 100644
index 0000000..53ee7a0
--- /dev/null
+++ b/doc/samples/login/login.css
@@ -0,0 +1,32 @@
+body {
+ max-width: 100ex;
+ margin: 0 auto;
+ background-color: white;
+ color: black;
+ text-align: center;
+}
+form > * {
+ display: block;
+ clear: both;
+ margin: 0.5ex auto;
+}
+label input {
+ display: block;
+ width: 50%;
+ float: right;
+ margin: 0 0 1ex;
+}
+#messages {
+ margin: 1em 0;
+ background-color: rgba(255,0,0,0.1);
+ border: 0.3ex outset rgba(255,0,0,0.1);
+ border-radius: 1ex;
+ padding: 0 1ex;
+ text-align: left;
+}
+#messages .info {
+ color: orangered;
+}
+#messages .warning {
+ color: blue;
+}
diff --git a/doc/samples/login/login.html b/doc/samples/login/login.html
new file mode 100644
index 0000000..151de65
--- /dev/null
+++ b/doc/samples/login/login.html
@@ -0,0 +1,14 @@
+
+ Login
+
+
+
+ Single Sign-On for example.org
+
+
+
diff --git a/doc/samples/login/login.js b/doc/samples/login/login.js
new file mode 100644
index 0000000..a688119
--- /dev/null
+++ b/doc/samples/login/login.js
@@ -0,0 +1 @@
+//JS
diff --git a/doc/samples/portal/portal.css b/doc/samples/portal/portal.css
new file mode 100644
index 0000000..b9f4552
--- /dev/null
+++ b/doc/samples/portal/portal.css
@@ -0,0 +1,50 @@
+body {
+ max-width: 100ex;
+ margin: 0 auto;
+ background-color: white;
+ color: black;
+ text-align: center;
+}
+#sites ul {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ row-gap: 1ex;
+ column-gap: 1ex;
+ margin: 0;
+}
+#sites li {
+ display: flex;
+ flex-grow: 1;
+ margin: 0;
+ padding: 0;
+}
+#sites ul::after {
+ content: "";
+ flex-grow: 10;
+}
+#sites a {
+ display: block;
+ position: relative;
+ flex-grow: 1;
+ min-height: 3em;
+ min-width: 5em;
+ max-width: 10em;
+ color: rgb(255, 255, 127);
+ background-color: rgb(70, 130, 180);
+ border: 0.3ex outset rgb(70, 130, 180);
+ border-radius: 1ex;
+ padding: 1ex;
+}
+#sites a span {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+#sites a:hover {
+ border-style: inset;
+ color: rgb(245, 245, 117);
+ background-color: rgb(65, 125, 175);
+}
diff --git a/doc/samples/portal/portal.html b/doc/samples/portal/portal.html
new file mode 100644
index 0000000..7d9baf6
--- /dev/null
+++ b/doc/samples/portal/portal.html
@@ -0,0 +1,9 @@
+
+ Single Sign-On Portal
+
+
+
+ Available pages for SSSO_USER
+ Welcome SSSO_NAME! Here are the pages you can open using your single sign-on:
+
+
diff --git a/doc/samples/portal/portal.js b/doc/samples/portal/portal.js
new file mode 100644
index 0000000..a688119
--- /dev/null
+++ b/doc/samples/portal/portal.js
@@ -0,0 +1 @@
+//JS
diff --git a/doc/samples/test_pages/private_restricted.php b/doc/samples/test_pages/private_restricted.php
new file mode 100644
index 0000000..5ccb6c1
--- /dev/null
+++ b/doc/samples/test_pages/private_restricted.php
@@ -0,0 +1,2 @@
+' .. util.str_to_html(cause) .. "
"
+ else
+ paras = ""
+ end
+ if warnings then
+ for _, m in ipairs(warnings) do
+ paras = paras .. '' .. util.str_to_html(m) .. "
"
+ end
+ end
+ if paras ~= "" then
+ html = html:gsub("", '", 1)
+ end
+ local back_url = req_data.query_params.back
+ if not back_url then
+ back_url = ""
+ end
+ html = html:gsub("BACK_URL", util.str_to_html(back_url))
+ return html
+end
+
+local function check_login(req_data)
+ req_data = nginx.with_post_parameters(req_data)
+ local user = req_data.query_params["login"] or ""
+ local password = req_data.query_params["password"] or ""
+ local user_data = auth.read_user(user, password)
+ if user_data then
+ log.debug("Credentials accepted for user " .. user_data.name)
+ local profile = prof.build_profile(user, password, user_data.name, user_data.email)
+ profile = sites.with_sites(profile, user)
+ log.debug("Building JWS")
+ local jws, tslimit = crypto.get_jws_and_tslimit(profile)
+ if not jws then
+ return nginx.answer_unexpected_error()
+ end
+ nginx.set_jws_cookie(jws, tslimit)
+ local back_url = req_data.query_params.back
+ log.debug("Redirecting")
+ if back_url then
+ return nginx.redirect_to_page(back_url)
+ else
+ return nginx.redirect_to_portal()
+ end
+ else
+ log.debug("Credentials rejected")
+ local html = inject_data(contents("login.html"), req_data, {
+ "These credentials were rejected."
+ })
+ return nginx.return_contents(html, "text/html; charset=UTF-8")
+ end
+end
+
+local function answer_request(req_data)
+ local login = conf.get_sso_prefix() .. "/login"
+ if nginx.is(req_data, login) then
+ if nginx.has_method(req_data, "POST") then
+ log.info("Checking login")
+ return check_login(req_data)
+ else
+ local html = inject_data(contents("login.html"), req_data, nil)
+ return nginx.return_contents(html, "text/html; charset=UTF-8")
+ end
+ elseif nginx.is(req_data, login .. ".css") then
+ return nginx.return_contents(contents("login.css"), "text/css; charset=UTF-8")
+ elseif nginx.is(req_data, login .. ".js") then
+ return nginx.return_contents(contents("login.js"), "application/javascript; charset=UTF-8")
+ else
+ log.info("Unknown login file")
+ return nginx.answer_not_found(req_data)
+ end
+end
+
+return {
+ answer_request = answer_request,
+ set_root = set_root,
+}
diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua
new file mode 100644
index 0000000..889c726
--- /dev/null
+++ b/src/ssso_nginx.lua
@@ -0,0 +1,156 @@
+local ngx = require("ngx")
+local util = require("ssso_util")
+local conf = require("ssso_config")
+
+local function get_request()
+ local vars = ngx.var
+ local request = {
+ referer = vars.http_referer,
+ host = vars.host,
+ method = vars.request_method,
+ uri = vars.request_uri,
+ }
+ if request.referer == "" then
+ request.referer = nil
+ end
+
+ local target, qp = request.uri, {}
+ local qm, _ = target:find("%?")
+ if qm then
+ qp = ngx.decode_args(target:sub(qm + 1))
+ target = target:sub(1, qm - 1)
+ end
+ request["target"] = target
+ request["query_params"] = qp
+
+ local https = vars.proxy_https or vars.https
+ if https and https ~= "" then
+ request["scheme"] = "https"
+ else
+ request["scheme"] = "http"
+ end
+ return request
+end
+
+local function with_post_parameters(req_data)
+ ngx.req.read_body()
+ local args, _ = ngx.req.get_post_args()
+ if args then
+ for key, val in pairs(args) do
+ req_data.query_params[key] = val
+ end
+ end
+ return req_data
+end
+
+local function get_jws_cookie()
+ return ngx.var.cookie_SSSO_TOKEN
+end
+
+local function set_jws_cookie(jws, tslimit)
+ ngx.header["Set-Cookie"] = "SSSO_TOKEN=" .. jws ..
+ "; Path=/; Expires=" .. ngx.cookie_time(tslimit) ..
+ "; Secure"
+end
+
+local function get_seconds_since_epoch()
+ return math.floor(ngx.now())
+end
+
+local function add_cookie(name, value)
+ local cookie = name .. "=" .. value
+ local old_cookie = ngx.var.http_cookie
+ if old_cookie and old_cookie ~= "" then
+ cookie = old_cookie .. "; " .. cookie
+ end
+ ngx.log(ngx.DEBUG, "Overriding request Cookie header: " .. cookie)
+ ngx.req.set_header("Cookie", cookie)
+end
+
+local function add_header(name, value)
+ ngx.log(ngx.DEBUG, "Setting request " .. name .. " header: " .. value)
+ ngx.req.set_header(name, value)
+end
+
+local function has_method(req_data, method)
+ return string.upper(method) == string.upper(req_data.method)
+end
+
+local function is(req_data, url)
+ return req_data.target == url
+end
+
+local function matches(req_data, lua_pattern)
+ return req_data.target:match(lua_pattern)
+end
+
+local function starts_with(req_data, prefix)
+ local i, _ = req_data.target:find(util.str_to_pattern(prefix))
+ return 1 == i
+end
+
+local function has_param(req_data, param, value)
+ return req_data.query_params[param] ~= nil and (value == nil or req_data.query_params[param] == value)
+end
+
+local function answer_not_found(req_data)
+ return ngx.exit(404)
+end
+
+local function answer_unexpected_error()
+ ngx.log(ngx.ERROR, "Unexpected Simple-SSO error.")
+ return ngx.exit(500)
+end
+
+local function redirect_to_page(uri)
+ return ngx.redirect("https://" .. conf.get_sso_host() .. uri, 302)
+end
+
+local function redirect_to_login(req_data, status)
+ return ngx.redirect("https://" ..
+ conf.get_sso_host() ..
+ conf.get_sso_prefix() ..
+ "/login?back=" ..
+ ngx.escape_uri(req_data.uri) ..
+ "&cause=" .. tostring(status),
+ 307)
+end
+
+local function redirect_to_portal()
+ return ngx.redirect("https://" .. conf.get_sso_host() .. conf.get_sso_prefix() .. "/portal", 307)
+end
+
+local function return_contents(contents, mime_and_charset)
+ ngx.header["Content-Type"] = mime_and_charset
+ ngx.header["Content-Length"] = tostring(#contents)
+ ngx.header["Cache-Control"] = "no-store,max-age=0"
+ ngx.say(contents)
+ -- TODO: CSRF
+ return ngx.exit(200)
+end
+
+local function forward_request(req_data)
+ return
+end
+
+return {
+ add_cookie = add_cookie,
+ add_header = add_header,
+ answer_not_found = answer_not_found,
+ forward_request = forward_request,
+ get_jws_cookie = get_jws_cookie,
+ get_request = get_request,
+ get_seconds_since_epoch = get_seconds_since_epoch,
+ has_method = has_method,
+ has_param = has_param,
+ is = is,
+ matches = matches,
+ redirect_to_login = redirect_to_login,
+ redirect_to_page = redirect_to_page,
+ redirect_to_portal = redirect_to_portal,
+ return_contents = return_contents,
+ set_jws_cookie = set_jws_cookie,
+ starts_with = starts_with,
+ answer_unexpected_error = answer_unexpected_error,
+ with_post_parameters = with_post_parameters,
+}
diff --git a/src/ssso_oauth2.lua b/src/ssso_oauth2.lua
new file mode 100644
index 0000000..028bb83
--- /dev/null
+++ b/src/ssso_oauth2.lua
@@ -0,0 +1,14 @@
+local function answer_oidc_webfinger(req_data)
+ -- TODO
+ return "TODO"
+end
+
+local function answer_request(req_data, auth)
+ -- TODO
+ return "TODO"
+end
+
+return {
+ answer_oidc_webfinger = answer_oidc_webfinger,
+ answer_request = answer_request,
+}
diff --git a/src/ssso_portal.lua b/src/ssso_portal.lua
new file mode 100644
index 0000000..5c6a9d3
--- /dev/null
+++ b/src/ssso_portal.lua
@@ -0,0 +1,55 @@
+local util = require("ssso_util")
+local conf = require("ssso_config")
+local nginx = require("ssso_nginx")
+local prof = require("ssso_profile")
+local sites = require("ssso_sites")
+
+local root = ""
+
+local function set_root(prefix)
+ root = prefix .. "/portal/"
+end
+
+local function contents(relative)
+ local file = assert(io.open(root .. relative, "r"), "Cannot open portal file " .. root .. relative)
+ local data = file:read("*all")
+ file:close()
+ return data
+end
+
+local function inject_data(html, profile)
+ html = html:gsub("SSSO_USER", util.str_to_html(prof.user(profile)))
+ html = html:gsub("SSSO_NAME", util.str_to_html(prof.name(profile)))
+ html = html:gsub("SSSO_EMAIL", util.str_to_html(prof.email(profile)))
+ local links = ""
+ local allowed = sites.authorized_links(prof.user(profile))
+ table.sort(allowed, function(a1, a2) return a1.label < a2.label end)
+ for _, allow in ipairs(allowed) do
+ links = links .. '' .. util.str_to_html(prof.format(allow.label, profile)) .. ""
+ end
+ if links ~= "" then
+ html = html:gsub('', '")
+ else
+ html = html:gsub('', 'No link to display.
')
+ end
+ return html
+end
+
+local function answer_request(req_data, profile)
+ local portal = conf.get_sso_prefix() .. "/portal"
+ if nginx.is(req_data, portal) then
+ local html = inject_data(contents("portal.html"), profile)
+ return nginx.return_contents(html, "text/html; charset=UTF-8")
+ elseif nginx.is(req_data, portal .. ".css") then
+ return nginx.return_contents(contents("portal.css"), "text/css; charset=UTF-8")
+ elseif nginx.is(req_data, portal .. ".js") then
+ return nginx.return_contents(contents("portal.js"), "application/javascript; charset=UTF-8")
+ else
+ return nginx.answer_not_found(req_data)
+ end
+end
+
+return {
+ answer_request = answer_request,
+ set_root = set_root,
+}
diff --git a/src/ssso_profile.lua b/src/ssso_profile.lua
new file mode 100644
index 0000000..e1c5ed1
--- /dev/null
+++ b/src/ssso_profile.lua
@@ -0,0 +1,63 @@
+local b64 = require("ssso_base64")
+
+local function build_profile(user, password, name, email)
+ return {
+ u = user,
+ p = password,
+ n = name,
+ e = email,
+ }
+end
+
+local function serialize(profile)
+ return (profile.u or "\025") .. "\031" ..
+ (profile.p or "\025") .. "\031" ..
+ (profile.n or "\025") .. "\031" ..
+ (profile.e or "\025") .. "\031"
+end
+
+local function deserialize(ser)
+ local profile
+ local remainder = ser:gsub("^(.-)\031(.-)\031(.-)\031(.-)\031", function (u, p, n, e)
+ if u == "\025" then u = nil end
+ if p == "\025" then p = nil end
+ if n == "\025" then n = nil end
+ if e == "\025" then e = nil end
+ profile = build_profile(u, p, n, e)
+ return ""
+ end)
+ return profile, remainder
+end
+
+local function format(template, profile)
+ local s = template
+ s = s:gsub("\ru%.", profile.u or "")
+ s = s:gsub("\rp%.", profile.p or "")
+ s = s:gsub("\rn%.", profile.n or "")
+ s = s:gsub("\re%.", profile.e or "")
+ s = s:gsub("\rb64%(([^\r]-)%)%.", function(x) return b64.encode_base64(x) end)
+ s = s:gsub("\ru64%(([^\r]-)%)%.", function(x) return b64.encode_base64url(x) end)
+ return s
+end
+
+local function email(profile)
+ return profile.e
+end
+
+local function name(profile)
+ return profile.n
+end
+
+local function user(profile)
+ return profile.u
+end
+
+return {
+ build_profile = build_profile,
+ deserialize = deserialize, -- TODO: test
+ serialize = serialize, -- TODO: test
+ email = email,
+ format = format,
+ name = name,
+ user = user,
+}
diff --git a/src/ssso_sessions.lua b/src/ssso_sessions.lua
new file mode 100644
index 0000000..b681943
--- /dev/null
+++ b/src/ssso_sessions.lua
@@ -0,0 +1,24 @@
+local crypto = require("ssso_crypto")
+local nginx = require("ssso_nginx")
+
+local function get_session()
+ local cookie = nginx.get_jws_cookie()
+
+ if not cookie or cookie == "" then
+ return nil, 401
+ end
+
+ local session, jws, tslimit = crypto.get_data_and_new_jws(cookie)
+
+ if session then
+ nginx.set_jws_cookie(jws, tslimit)
+ return session, 200
+ else
+ return nil, 403
+ end
+end
+
+
+return {
+ get_session = get_session,
+}
diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua
new file mode 100644
index 0000000..c9a8091
--- /dev/null
+++ b/src/ssso_sites.lua
@@ -0,0 +1,215 @@
+local json = require("cjson.safe")
+local nginx = require("ssso_nginx")
+local prof = require("ssso_profile")
+
+local known_private_re = {}
+local known_sites = {}
+
+local function load_sites(prefix)
+ local f, site
+ local ls = assert(io.popen("/bin/ls -f1Nb \"" .. prefix .. "/sites\"/*.json", "r"), "popen is required")
+ for name in ls:lines() do
+ f = assert(io.open(name, "r"), "File " .. name .. " cannot be read")
+ site = assert(json.decode(f:read("*all")))
+ f:close()
+ table.insert(known_sites, name)
+ for _, pat in ipairs(site.patterns) do
+ if not pat.public then
+ for _, r in ipairs(pat.lua_regex) do
+ table.insert(known_private_re, r)
+ end
+ end
+ end
+ end
+ ls:close()
+end
+
+local function is_known_private(req_data)
+ for _, r in ipairs(known_private_re) do
+ if nginx.matches(req_data, r) then
+ return true
+ end
+ end
+ return false
+end
+
+local function handle_request(req_data, auth)
+ if auth then
+ for _, site in ipairs(auth.ok) do
+ for _, r in ipairs(site.r) do
+ if nginx.matches(req_data, r) then
+ for _, a in ipairs(site.a) do
+ if a[1] == "C" then
+ nginx.add_cookie(a[2], prof.format(a[3], auth))
+ elseif a[1] == "H" then
+ nginx.add_header(a[2], prof.format(a[3], auth))
+ end
+ end
+ return nginx.forward_request(req_data)
+ end
+ end
+ end
+ for _, r in ipairs(auth.ko) do
+ if nginx.matches(req_data, r) then
+ return nginx.redirect_to_login(req_data, 403)
+ end
+ end
+ return nginx.forward_request(req_data)
+ elseif is_known_private(req_data) then
+ return nginx.redirect_to_login(req_data, 401)
+ else
+ return nginx.forward_request(req_data)
+ end
+end
+
+local function format_pattern(pattern)
+ local a_type
+ local ok = {
+ r = pattern.lua_regex or {},
+ a = {},
+ }
+ for _, action in ipairs(pattern.actions or {}) do
+ if action.type == "header" then
+ a_type = "H"
+ elseif action.type == "cookie" then
+ a_type = "C"
+ else
+ a_type = nil
+ end
+ if a_type then
+ table.insert(ok.a, {a_type, action.name, action.value})
+ end
+ end
+ return ok
+end
+
+local function with_sites(profile, user)
+ local f, site, go_on
+ local ok_list = {}
+ local ko_list = {}
+ for _, name in ipairs(known_sites) do
+ f = io.open(name, "r")
+ if f then
+ site = json.decode(f:read("*all"))
+ f:close()
+ for _, pat in ipairs(site.patterns or {}) do
+ go_on = true
+ for _, denied in ipairs(pat.deny or {}) do
+ if denied == user then
+ go_on = false
+ for _, re in ipairs(pat.lua_regex or {}) do
+ table.insert(ko_list, re)
+ end
+ break
+ end
+ end
+ if go_on then
+ if pat.public then
+ local ok = format_pattern(pat)
+ table.insert(ok_list, ok)
+ else
+ for _, allowed in ipairs(pat.allow or {}) do
+ if allowed == "*" or allowed == user then
+ local ok = format_pattern(pat)
+ table.insert(ok_list, ok)
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ profile["ok"] = ok_list
+ profile["ko"] = ko_list
+ return profile
+end
+
+local function serialize(profile)
+ local ser_s = ""
+ for _, site in ipairs(profile.ok or {}) do
+ for _, r in ipairs(site.r) do
+ ser_s = ser_s .. r .. "\029"
+ end
+ for _, a in ipairs(site.a) do
+ ser_s = ser_s .. a[1] .. a[2] .. "=" .. a[3] .. "\028"
+ end
+ ser_s = ser_s .. "\031"
+ end
+ for _, r in ipairs(profile.ko or {}) do
+ ser_s = ser_s .. r .. "\030"
+ end
+ return ser_s
+end
+
+local function deserialize_update(ser, profile)
+ if not ser or ser == "" then
+ return profile
+ end
+ local ok_list = {}
+ local ko_list = {}
+ local remainder = ser:gsub("(.-)\031", function (ser_ok)
+ local ok = {
+ r = {},
+ a = {},
+ }
+ ser_ok = ser_ok:gsub("(.-)\029", function(r) table.insert(ok.r, r); return "" end)
+ ser_ok:gsub("(.)([^=]-)=(.-)\028", function(t, n, v) table.insert(ok.a, {t, n, v}) end)
+ table.insert(ok_list, ok)
+ return ""
+ end)
+ remainder = remainder:gsub("(.-)\030", function (ko)
+ table.insert(ko_list, ko)
+ return ""
+ end)
+ profile.ok = ok_list
+ profile.ko = ko_list
+ return profile, remainder
+end
+
+local function authorized_links(user)
+ local links = {}
+ local f, site, go_on
+ for _, name in ipairs(known_sites) do
+ f = io.open(name, "r")
+ if f then
+ site = json.decode(f:read("*all"))
+ f:close()
+ for _, pat in ipairs(site.patterns or {}) do
+ go_on = true
+ for _, denied in ipairs(pat.deny or {}) do
+ if denied == user then
+ go_on = false
+ break
+ end
+ end
+ if go_on then
+ if pat.public then
+ for link, label in pairs(pat.portal or {}) do
+ table.insert(links, {link = link, label = label})
+ end
+ else
+ for _, allowed in ipairs(pat.allow or {}) do
+ if allowed == "*" or allowed == user then
+ for link, label in pairs(pat.portal or {}) do
+ table.insert(links, {link = link, label = label})
+ end
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ return links
+end
+
+return {
+ authorized_links = authorized_links,
+ deserialize_update = deserialize_update, -- TODO: test
+ handle_request = handle_request,
+ load_sites = load_sites,
+ serialize = serialize, -- TODO: test
+ with_sites = with_sites,
+}
diff --git a/src/ssso_util.lua b/src/ssso_util.lua
new file mode 100644
index 0000000..98f4107
--- /dev/null
+++ b/src/ssso_util.lua
@@ -0,0 +1,18 @@
+local function str_to_html(s)
+ s = s:gsub("&", "&")
+ s = s:gsub("<", "<")
+ s = s:gsub(">", ">")
+ s = s:gsub('"', """)
+ s = s:gsub('%%', "%") -- avoid unwanted substitutions
+ return s
+end
+
+local function str_to_pattern(s)
+ s = s:gsub("([%(%)%%%.%+%-%*%?%^%$])", "%%%1")
+ return s
+end
+
+return {
+ str_to_html = str_to_html,
+ str_to_pattern = str_to_pattern,
+}
diff --git a/test/aes.utest.lua b/test/aes.utest.lua
new file mode 100644
index 0000000..dc968ba
--- /dev/null
+++ b/test/aes.utest.lua
@@ -0,0 +1,23 @@
+local lu = require("luaunit")
+local aes = require("resty.openssl.cipher")
+
+function test_aes()
+ local aes1 = aes.new(nil)
+ local aes2 = aes.new(nil)
+ local enc1 = assert(aes1:encrypt("a", nil, "test", nil, nil))
+ local enc2 = assert(aes2:encrypt("b", nil, "other", nil, nil))
+ local tag1 = aes1:get_aead_tag()
+ local tag2 = aes2:get_aead_tag()
+ local aes3 = aes.new(nil)
+ local aes4 = aes.new(nil)
+ lu.assertEquals(#tag1, 16)
+ lu.assertEquals(#tag2, 16)
+ lu.assertNotEquals(enc1, "test")
+ lu.assertNotEquals(enc2, "other")
+ lu.assertNotEquals(enc1 .. tag1, "test")
+ lu.assertNotEquals(enc2 .. tag2, "other")
+ lu.assertEquals(aes3:decrypt("a", nil, enc1, nil, nil, tag1), "test")
+ lu.assertEquals(aes4:decrypt("b", nil, enc2, nil, nil, tag2), "other")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/alt/bit.lua b/test/alt/bit.lua
new file mode 100644
index 0000000..1fd2377
--- /dev/null
+++ b/test/alt/bit.lua
@@ -0,0 +1 @@
+return require("bit32")
diff --git a/test/alt/ngx.lua b/test/alt/ngx.lua
new file mode 100644
index 0000000..f56bdd2
--- /dev/null
+++ b/test/alt/ngx.lua
@@ -0,0 +1,135 @@
+local var = {}
+local post_var = {}
+local header = {}
+local req_header = {}
+local resp_body = nil
+
+local function cookie_time(time)
+ return tostring(time)
+end
+
+local function now()
+ return 1626546790.456
+end
+
+local function set_req_header(k, v)
+ req_header[k] = v
+ var["http_" .. k:lower()] = v
+end
+
+local function get_req_header(_, k)
+ return req_header[k]
+end
+
+local function reset_req_header()
+ req_header = {}
+end
+
+local function set_header(_, k, v)
+ if k == "Set-Cookie" then
+ header[k] = {v = v, link = header[k]}
+ else
+ header[k] = v
+ end
+end
+
+local function get_header(_, k)
+ return header[k]
+end
+
+local function reset_header()
+ header = {}
+end
+
+local function set_var(_, k, v)
+ var[k] = v
+end
+
+local function get_var(_, k)
+ return var[k]
+end
+
+local function reset_var()
+ var = {}
+end
+
+local function set_post_var(_, k, v)
+ post_var[k] = v
+end
+
+local function get_post_var(_, k)
+ return post_var[k]
+end
+
+local function reset_post_var()
+ post_var = {}
+end
+
+local function decode_args(argstr)
+ local params = {}
+ for p in argstr:gmatch("([^?&=]+[^?&]*)") do
+ local eq, _ = p:find("=")
+ if eq then
+ params[p:sub(1, eq - 1)] = p:sub(eq + 1):lower() -- lower → fake URL-decoding
+ else
+ params[p] = true
+ end
+ end
+ return params
+end
+
+local function escape_uri(uri)
+ return uri
+end
+
+local function exit(status)
+ return status
+end
+
+local function redirect(url, status)
+ return tostring(status) .. ":" .. url
+end
+
+local function reset_resp_body()
+ resp_body = nil
+end
+
+local function say(set_or_get)
+ if set_or_get then
+ resp_body = set_or_get
+ else
+ return resp_body
+ end
+end
+
+local function log(level, message)
+ print(level .. message)
+end
+
+return {
+ cookie_time = cookie_time,
+ decode_args = decode_args,
+ exit = exit,
+ escape_uri = escape_uri,
+ header = setmetatable({}, {__newindex = set_header, __index = get_header}),
+ log = log,
+ DEBUG = "DEBUG: ",
+ INFO = "INFO: ",
+ ERROR = "ERROR: ",
+ now = now,
+ post_var = setmetatable({}, {__newindex = set_post_var, __index = get_post_var}),
+ redirect = redirect,
+ req = {
+ get_post_args = function() return post_var end,
+ header = setmetatable({}, {__newindex = function(_,k,v) set_req_header(k,v) end, __index = get_req_header}),
+ read_body = function() end,
+ reset = reset_req_header,
+ set_header = set_req_header,
+ },
+ reset_header = reset_header,
+ reset_post_var = reset_post_var,
+ reset_resp_body = reset_resp_body,
+ reset_var = reset_var,
+ say = say,
+ var = setmetatable({}, {__newindex = set_var, __index = get_var}),
+}
diff --git a/test/alt/ngx/base64.lua b/test/alt/ngx/base64.lua
new file mode 100644
index 0000000..51096be
--- /dev/null
+++ b/test/alt/ngx/base64.lua
@@ -0,0 +1,18 @@
+local b64 = require("base64")
+
+local function decode_base64url(base64)
+ base64 = base64:gsub("-", "+")
+ base64 = base64:gsub("_", "/")
+ return b64.decode(base64)
+end
+
+local function encode_base64url(plaintext)
+ local plain = b64.encode(plaintext)
+ plain = plain:gsub("/", "_")
+ return plain:gsub("%+", "-")
+end
+
+return {
+ decode_base64url = decode_base64url,
+ encode_base64url = encode_base64url,
+}
diff --git a/test/alt/resty/openssl/cipher.lua b/test/alt/resty/openssl/cipher.lua
new file mode 100644
index 0000000..6200328
--- /dev/null
+++ b/test/alt/resty/openssl/cipher.lua
@@ -0,0 +1,35 @@
+local real_aes = require("resty.easy-crypto")
+
+local function new(_)
+ local fake_instance = {}
+
+ function fake_instance:encrypt(key, _, data, _, _)
+ local aes = real_aes:new({
+ saltSize = 16,
+ ivSize = 12,
+ iterationCount = 2,
+ })
+ local encrypted = assert(aes:encrypt(key, data))
+ self.tag = encrypted:sub(-16)
+ return encrypted:sub(1, -17), nil
+ end
+
+ function fake_instance:get_aead_tag()
+ return self.tag
+ end
+
+ function fake_instance:decrypt(key, _, data, _, _, tag)
+ local aes = real_aes:new({
+ saltSize = 16,
+ ivSize = 12,
+ iterationCount = 2,
+ })
+ return aes:decrypt(key, data .. tag)
+ end
+
+ return fake_instance
+end
+
+return {
+ new = new,
+}
diff --git a/test/alt/resty/random.lua b/test/alt/resty/random.lua
new file mode 100644
index 0000000..86434b8
--- /dev/null
+++ b/test/alt/resty/random.lua
@@ -0,0 +1,9 @@
+local ssl_rand = require("openssl.rand")
+
+local function bytes(count, _)
+ return ssl_rand.bytes(count)
+end
+
+return {
+ bytes = bytes,
+}
diff --git a/test/alt/resty/sha256.lua b/test/alt/resty/sha256.lua
new file mode 100644
index 0000000..03b7ba4
--- /dev/null
+++ b/test/alt/resty/sha256.lua
@@ -0,0 +1,20 @@
+local real_sha = require("bgcrypto.sha256")
+local sha_proxy = {}
+
+function sha_proxy:new()
+ local fake_instance = {
+ data = "",
+ }
+
+ function fake_instance:update(data)
+ self.data = self.data .. data
+ end
+
+ function fake_instance:final()
+ return real_sha.digest(self.data, true)
+ end
+
+ return fake_instance
+end
+
+return sha_proxy
diff --git a/test/anonymous1.ctest.lua b/test/anonymous1.ctest.lua
new file mode 100644
index 0000000..acfb01b
--- /dev/null
+++ b/test/anonymous1.ctest.lua
@@ -0,0 +1,18 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_anonymous_access_to_unknown_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/unknown"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertTrue(resp) -- require changes nil into true
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/anonymous2.ctest.lua b/test/anonymous2.ctest.lua
new file mode 100644
index 0000000..5ce6f9f
--- /dev/null
+++ b/test/anonymous2.ctest.lua
@@ -0,0 +1,18 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_anonymous_access_to_public_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/public/page"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertTrue(resp) -- require changes nil into true
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/anonymous3.ctest.lua b/test/anonymous3.ctest.lua
new file mode 100644
index 0000000..9986310
--- /dev/null
+++ b/test/anonymous3.ctest.lua
@@ -0,0 +1,18 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_anonymous_access_to_public_page_of_mixed_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertTrue(resp) -- require changes nil into true
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/anonymous4.ctest.lua b/test/anonymous4.ctest.lua
new file mode 100644
index 0000000..7210893
--- /dev/null
+++ b/test/anonymous4.ctest.lua
@@ -0,0 +1,18 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_anonymous_access_to_private_page_of_mixed_site_sent_to_login_401()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/_new"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/mixed/bob/wiki/_new&cause=401")
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/anonymous5.ctest.lua b/test/anonymous5.ctest.lua
new file mode 100644
index 0000000..89fcfe8
--- /dev/null
+++ b/test/anonymous5.ctest.lua
@@ -0,0 +1,18 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_anonymous_access_to_private_site_sent_to_login_401()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/private/page"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=401")
+ lu.assertNil(ngx.req.header["Authorization"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/auth.utest.lua b/test/auth.utest.lua
new file mode 100644
index 0000000..9d4472e
--- /dev/null
+++ b/test/auth.utest.lua
@@ -0,0 +1,19 @@
+local lu = require("luaunit")
+local auth = require("ssso_auth")
+local conf = require("ssso_config")
+
+local here = debug.getinfo(1).source:sub(2, -16)
+conf.load_conf(here)
+
+function test_read_user_with_good_credentials_returns_name_and_email()
+ lu.assertEquals(auth.read_user("U", "goodpassword"), {
+ name = "U",
+ email = "U@example.org",
+ })
+end
+
+function test_read_user_with_bad_credentials_returns_nil()
+ lu.assertNil(auth.read_user("U", "badpassword"))
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/config.utest.lua b/test/config.utest.lua
new file mode 100644
index 0000000..15c2694
--- /dev/null
+++ b/test/config.utest.lua
@@ -0,0 +1,16 @@
+local lu = require("luaunit")
+local conf = require("ssso_config")
+
+local here = debug.getinfo(1).source:sub(2, -18)
+conf.load_conf(here)
+
+function test_config()
+ lu.assertEquals(conf.get_auth_commands(), {
+ check = "if [ \"\rp.\" == \"goodpassword\" ]; then printf '%s\\n%s\\n' '\ru.' '\ru.@example.org'; fi",
+ })
+ lu.assertEquals(conf.get_session_seconds(), 3600)
+ lu.assertEquals(conf.get_sso_host(), "my-domain.tld")
+ lu.assertEquals(conf.get_sso_prefix(), "/ssso")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/crypto.utest.lua b/test/crypto.utest.lua
new file mode 100644
index 0000000..eae8cc1
--- /dev/null
+++ b/test/crypto.utest.lua
@@ -0,0 +1,50 @@
+local lu = require("luaunit")
+local conf = require("ssso_config")
+local crypt = require("ssso_crypto")
+
+local here = debug.getinfo(1).source:sub(2, -18)
+conf.load_conf(here)
+
+local data = {
+ u = "u",
+ e = "u@h",
+ ok = {
+ {
+ r = {
+ "regex1",
+ },
+ a = {
+ {"C", "Cn", "Cv"},
+ {"H", "Hn", "Hv"},
+ }
+ },
+ },
+ ko = {
+ "regex2",
+ },
+}
+
+function test_jws_is_well_structured()
+ local jws, _ = crypt.get_jws_and_tslimit(data)
+ lu.assertStrMatches(jws, "[^%.]+%.[^%.]+%.[^%.]+")
+end
+
+function test_jws_can_be_decoded()
+ local jws, _ = crypt.get_jws_and_tslimit(data)
+ local stored, _, _ = crypt.get_data_and_new_jws(jws)
+ lu.assertEquals(stored, data)
+end
+
+function test_data_must_contain_field_u()
+ local wrong = {
+ i = 1,
+ f = 2.3,
+ b = true,
+ n = nil,
+ }
+ local jws, ts = crypt.get_jws_and_tslimit(wrong)
+ lu.assertNil(jws)
+ lu.assertNil(ts)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/global.json b/test/global.json
new file mode 100644
index 0000000..3c5296d
--- /dev/null
+++ b/test/global.json
@@ -0,0 +1,8 @@
+{
+ "auth": {
+ "check": "if [ \"\rp.\" == \"goodpassword\" ]; then printf '%s\\n%s\\n' '\ru.' '\ru.@example.org'; fi"
+ },
+ "session_seconds": 3600,
+ "sso_host": "my-domain.tld",
+ "sso_prefix": "/ssso"
+}
diff --git a/test/login.utest.lua b/test/login.utest.lua
new file mode 100644
index 0000000..4f8e367
--- /dev/null
+++ b/test/login.utest.lua
@@ -0,0 +1,203 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local conf = require("ssso_config")
+local login = require("ssso_login")
+local ng = require("ssso_nginx")
+
+local here = debug.getinfo(1).source:sub(2, -17)
+conf.load_conf(here)
+login.set_root(here)
+
+function test_get_login_url_returns_html_with_back_url_substitution()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login?back=/somewhere"
+ local r = ng.get_request()
+ local expected = [[
+
+
+
+
+
+
+]]
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_login_css_url_returns_css()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "BLABLA"
+ ngx.var.request_uri = "/ssso/login.css"
+ local r = ng.get_request()
+ local expected = "/*CSS*/\n"
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_login_js_url_returns_js()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "BLABLA"
+ ngx.var.request_uri = "/ssso/login.js"
+ local r = ng.get_request()
+ local expected = "//JS\n"
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_unknown_login_url_returns_404()
+ -- given
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "BLABLA"
+ ngx.var.request_uri = "/ssso/login/unknown"
+ local r = ng.get_request()
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 404)
+ lu.assertNil(ngx.say())
+end
+
+function test_get_login_url_with_cause_401_returns_html_with_associated_message()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login?cause=401"
+ local r = ng.get_request()
+ local expected = [[
+
+
+
+ Credentials are needed to access this resource.
+
+
+]]
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_get_login_url_with_cause_403_returns_html_with_associated_message()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login?cause=403"
+ local r = ng.get_request()
+ local expected = [[
+
+
+
+ Different credentials might grant access to this resource.
+
+
+]]
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_post_login_url_with_wrong_credentials_returns_html_with_associated_message()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ local r = ng.get_request()
+ local expected = [[
+
+
+
+ These credentials were rejected.
+
+
+]]
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+function test_post_login_url_with_good_credentials_redirects_to_portal_with_session_cookie()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ ngx.post_var.login = "goodlogin"
+ ngx.post_var.password = "goodpassword"
+ local r = ng.get_request()
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/portal")
+ lu.assertNil(ngx.say())
+ lu.assertNil(ngx.header["Set-Cookie"].link)
+ lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure")
+end
+
+function test_post_login_url_with_good_credentials_and_back_url_redirects_to_given_url_with_session_cookie()
+ -- given
+ ngx.reset_header()
+ ngx.reset_var()
+ ngx.reset_resp_body()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ ngx.post_var.login = "goodlogin"
+ ngx.post_var.password = "goodpassword"
+ ngx.post_var.back = "/somewhere"
+ local r = ng.get_request()
+ -- when
+ local resp = login.answer_request(r)
+ -- then
+ lu.assertEquals(resp, "302:https://my-domain.tld/somewhere")
+ lu.assertNil(ngx.say())
+ lu.assertNil(ngx.header["Set-Cookie"].link)
+ lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login/login.css b/test/login/login.css
new file mode 100644
index 0000000..89f2b16
--- /dev/null
+++ b/test/login/login.css
@@ -0,0 +1 @@
+/*CSS*/
diff --git a/test/login/login.html b/test/login/login.html
new file mode 100644
index 0000000..cc731c0
--- /dev/null
+++ b/test/login/login.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/test/login/login.js b/test/login/login.js
new file mode 100644
index 0000000..a688119
--- /dev/null
+++ b/test/login/login.js
@@ -0,0 +1 @@
+//JS
diff --git a/test/login1.ctest.lua b/test/login1.ctest.lua
new file mode 100644
index 0000000..59c08d0
--- /dev/null
+++ b/test/login1.ctest.lua
@@ -0,0 +1,28 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_login_url_returns_html_with_cause_displayed()
+ -- given
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login?back=/private/page&cause=403"
+ local expected = [[
+
+
+
+ Different credentials might grant access to this resource.
+
+
+]]
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login2.ctest.lua b/test/login2.ctest.lua
new file mode 100644
index 0000000..a131c13
--- /dev/null
+++ b/test/login2.ctest.lua
@@ -0,0 +1,21 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_login_css_url_returns_css()
+ -- given
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login.css"
+ local expected = "/*CSS*/\n"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+ lu.assertEquals(ngx.say(), expected)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login3.ctest.lua b/test/login3.ctest.lua
new file mode 100644
index 0000000..dc101cc
--- /dev/null
+++ b/test/login3.ctest.lua
@@ -0,0 +1,21 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_login_js_url_returns_js()
+ -- given
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login.js"
+ local expected = "//JS\n"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+ lu.assertEquals(ngx.say(), expected)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login4.ctest.lua b/test/login4.ctest.lua
new file mode 100644
index 0000000..c227f61
--- /dev/null
+++ b/test/login4.ctest.lua
@@ -0,0 +1,20 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_unknown_login_url_returns_404()
+ -- given
+ ngx.reset_resp_body()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/login.html"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 404)
+ lu.assertNil(ngx.header["Content-Type"])
+ lu.assertNil(ngx.header["Content-Length"])
+ lu.assertNil(ngx.say())
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login5.ctest.lua b/test/login5.ctest.lua
new file mode 100644
index 0000000..6edc285
--- /dev/null
+++ b/test/login5.ctest.lua
@@ -0,0 +1,23 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_successful_login_with_back_url_goes_to_url()
+ -- given
+ ngx.reset_var()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ ngx.post_var.login = "goodlogin"
+ ngx.post_var.password = "goodpassword"
+ ngx.post_var.back = "/private/page"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, "302:https://my-domain.tld/private/page")
+ lu.assertNil(ngx.header["Set-Cookie"].link)
+ lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login6.ctest.lua b/test/login6.ctest.lua
new file mode 100644
index 0000000..2da72aa
--- /dev/null
+++ b/test/login6.ctest.lua
@@ -0,0 +1,32 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_failed_login_returns_to_login_with_message()
+ -- given
+ ngx.reset_var()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ ngx.post_var.login = "goodlogin"
+ ngx.post_var.password = "badpassword"
+ ngx.post_var.back = "/private/page"
+ local expected = [[
+
+
+
+ These credentials were rejected.
+
+
+]]
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/login7.ctest.lua b/test/login7.ctest.lua
new file mode 100644
index 0000000..91300b5
--- /dev/null
+++ b/test/login7.ctest.lua
@@ -0,0 +1,22 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+
+require("do_init")
+
+function test_successful_login_without_back_url_goes_to_portal()
+ -- given
+ ngx.reset_var()
+ ngx.reset_post_var()
+ ngx.var.request_method = "POST"
+ ngx.var.request_uri = "/ssso/login"
+ ngx.post_var.login = "goodlogin"
+ ngx.post_var.password = "goodpassword"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/portal")
+ lu.assertNil(ngx.header["Set-Cookie"].link)
+ lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/nginx.utest.lua b/test/nginx.utest.lua
new file mode 100644
index 0000000..fbbe1d5
--- /dev/null
+++ b/test/nginx.utest.lua
@@ -0,0 +1,313 @@
+local lu = require("luaunit")
+local conf = require("ssso_config")
+
+local here = debug.getinfo(1).source:sub(2, -17)
+conf.load_conf(here)
+
+local ngx = require("ngx")
+local ng = require("ssso_nginx")
+
+function test_refe_host_meth_uri_taken_from_ngx()
+ -- given
+ ngx.reset_var()
+ ngx.var.http_referer = "R"
+ ngx.var.host = "H"
+ ngx.var.request_method = "M"
+ ngx.var.request_uri = "U"
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.referer, "R")
+ lu.assertEquals(r.host, "H")
+ lu.assertEquals(r.method, "M")
+ lu.assertEquals(r.uri, "U")
+ lu.assertEquals(r.target, "U")
+ lu.assertEquals(r.query_params, {})
+end
+
+function test_empty_referer_reported_as_nil()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U"
+ ngx.var.http_referer = ""
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.referer, nil)
+end
+
+function test_query_params_split_from_uri_and_decoded()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U?P=V&Q=W"
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.uri, "U?P=V&Q=W")
+ lu.assertEquals(r.target, "U")
+ lu.assertEquals(r.query_params, {P = "v", Q = "w"})
+end
+
+function test_default_scheme_is_http()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U"
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.scheme, "http")
+end
+
+function test_scheme_is_https_when_proxy_https_var()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U"
+ ngx.var.proxy_https = 1
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.scheme, "https")
+end
+
+function test_scheme_is_https_when_https_var()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U"
+ ngx.var.https = 1
+ -- when
+ local r = ng.get_request()
+ -- then
+ lu.assertEquals(r.scheme, "https")
+end
+
+function test_get_jws_cookie_returns_the_cookie_from_nginx()
+ -- given
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = "cookie"
+ -- when
+ local c = ng.get_jws_cookie()
+ -- then
+ lu.assertEquals(c, "cookie")
+end
+
+function test_jws_cookie_sent_back_through_ngx()
+ -- given
+ ngx.reset_header()
+ -- when
+ ng.set_jws_cookie("J", 999)
+ -- then
+ lu.assertEquals(ngx.header["Set-Cookie"], {v = "SSSO_TOKEN=J; Path=/; Expires=999; Secure", link = nil})
+end
+
+function test_ngx_now_converted_to_integer()
+ lu.assertEquals(ng.get_seconds_since_epoch(), 1626546790)
+end
+
+function test_add_cookie_works_once()
+ -- given
+ ngx.reset_var()
+ ngx.req.reset()
+ -- when
+ ng.add_cookie("C", "V")
+ -- then
+ lu.assertEquals(ngx.req.header["Cookie"], "C=V")
+end
+
+function test_add_cookie_works_twice()
+ -- given
+ ngx.reset_var()
+ ngx.req.reset()
+ -- when
+ ng.add_cookie("C1", "V1")
+ ng.add_cookie("C2", "V2")
+ -- then
+ lu.assertEquals(ngx.req.header["Cookie"], "C1=V1; C2=V2")
+end
+
+function test_add_header_works()
+ -- given
+ ngx.reset_var()
+ ngx.req.reset()
+ -- when
+ ng.add_header("H1", "V1")
+ ng.add_header("H2", "V2")
+ ng.add_header("H1", "V3")
+ -- then
+ lu.assertEquals(ngx.req.header["H1"], "V3")
+ lu.assertEquals(ngx.req.header["H2"], "V2")
+end
+
+function test_method_is_recognized_case_insensitive()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_method = "get"
+ ngx.var.request_uri = "U"
+ local r = ng.get_request()
+ -- when
+ local is_get = ng.has_method(r, "GET")
+ local is_post = ng.has_method(r, "POST")
+ -- then
+ lu.assertTrue(is_get and true)
+ lu.assertFalse(is_post or false)
+end
+
+function test_uri_identity_ignores_query_parameters()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "U?P=V&Q=W"
+ local r = ng.get_request()
+ -- when
+ local is_without_qp = ng.is(r, "U")
+ local is_with_qp = ng.is(r, "U?P=V&Q=W")
+ -- then
+ lu.assertTrue(is_without_qp and true)
+ lu.assertFalse(is_with_qp or false)
+end
+
+function test_uri_match_ignores_query_parameters()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb"
+ local r = ng.get_request()
+ -- when
+ local matches_without_qp = ng.matches(r, "/a+$")
+ local matches_with_qp = ng.matches(r, "/a.*b")
+ -- then
+ lu.assertTrue(matches_without_qp and true)
+ lu.assertFalse(matches_with_qp or false)
+end
+
+function test_starts_with_ignores_query_parameters()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb"
+ local r = ng.get_request()
+ -- when
+ local start_without_qp = ng.starts_with(r, "/a")
+ local start_with_qp = ng.starts_with(r, "/aa?b")
+ -- then
+ lu.assertTrue(start_without_qp and true)
+ lu.assertFalse(start_with_qp or false)
+end
+
+function test_starts_with_must_start_with_given_value()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb"
+ local r = ng.get_request()
+ -- when
+ local start_in_middle = ng.starts_with(r, "aa")
+ local does_not_start = ng.starts_with(r, "x")
+ -- then
+ lu.assertFalse(start_in_middle or false)
+ lu.assertFalse(does_not_start or false)
+end
+
+function test_has_param_works_disregarding_the_value()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb&c=1"
+ local r = ng.get_request()
+ -- when
+ local has_unknown_param = ng.has_param(r, "b")
+ local has_unvalued_param = ng.has_param(r, "bb")
+ local has_valued_param = ng.has_param(r, "c")
+ -- then
+ lu.assertFalse(has_unknown_param or false)
+ lu.assertTrue(has_unvalued_param and true)
+ lu.assertTrue(has_valued_param and true)
+end
+
+function test_has_param_works_with_a_correct_value()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb&c=1"
+ local r = ng.get_request()
+ -- when
+ local has_unvalued_param = ng.has_param(r, "bb", true)
+ local has_valued_param = ng.has_param(r, "c", "1")
+ -- then
+ lu.assertTrue(has_unvalued_param and true)
+ lu.assertTrue(has_valued_param and true)
+end
+
+function test_has_param_works_with_a_wrong_value()
+ -- given
+ ngx.reset_var()
+ ngx.var.request_uri = "/aa?bb&c=1"
+ local r = ng.get_request()
+ -- when
+ local has_unknown_param = ng.has_param(r, "b", "x")
+ local has_unvalued_param = ng.has_param(r, "bb", "x")
+ local has_valued_param = ng.has_param(r, "c", "x")
+ -- then
+ lu.assertFalse(has_unknown_param or false)
+ lu.assertFalse(has_unvalued_param or false)
+ lu.assertFalse(has_valued_param or false)
+end
+
+function test_answer_not_found_returns_404()
+ lu.assertEquals(ng.answer_not_found(), 404)
+end
+
+function test_answer_unexpected_error_returns_500()
+ lu.assertEquals(ng.answer_unexpected_error(), 500)
+end
+
+function test_redirect_to_page_prepends_host()
+ lu.assertEquals(ng.redirect_to_page("/url"), "302:https://my-domain.tld/url")
+end
+
+function test_redirect_to_portal()
+ lu.assertEquals(ng.redirect_to_portal(), "307:https://my-domain.tld/ssso/portal")
+end
+
+function test_return_contents_returns_data_and_headers_http_200()
+ -- given
+ ngx.reset_resp_body()
+ ngx.reset_header()
+ -- when
+ local resp = ng.return_contents("Contents", "Mime; Charset")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), "Contents")
+ lu.assertEquals(ngx.header["Content-Type"], "Mime; Charset")
+ lu.assertEquals(ngx.header["Content-Length"], "8")
+ lu.assertEquals(ngx.header["Cache-Control"], "no-store,max-age=0")
+end
+
+function test_with_post_parameters_merges_post_parameters_to_request_data()
+ -- given
+ ngx.reset_post_var()
+ ngx.reset_var()
+ ngx.var.request_uri = "url?p&q=3"
+ ngx.post_var.p = "5"
+ ngx.post_var.r = "hello"
+ -- when (1)
+ local r = ng.get_request()
+ -- then (1)
+ lu.assertEquals(r, {
+ scheme = "http",
+ uri = "url?p&q=3",
+ target = "url",
+ query_params = {
+ p = true,
+ q = "3",
+ },
+ })
+ -- when (2)
+ r = ng.with_post_parameters(r)
+ -- then (2)
+ lu.assertEquals(r, {
+ scheme = "http",
+ uri = "url?p&q=3",
+ target = "url",
+ query_params = {
+ p = "5",
+ q = "3",
+ r = "hello",
+ },
+ })
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/portal/portal.css b/test/portal/portal.css
new file mode 100644
index 0000000..89f2b16
--- /dev/null
+++ b/test/portal/portal.css
@@ -0,0 +1 @@
+/*CSS*/
diff --git a/test/portal/portal.html b/test/portal/portal.html
new file mode 100644
index 0000000..a961516
--- /dev/null
+++ b/test/portal/portal.html
@@ -0,0 +1,7 @@
+
+
+
+
+ SSSO_USER SSSO_NAME SSSO_EMAIL
+
+
diff --git a/test/portal/portal.js b/test/portal/portal.js
new file mode 100644
index 0000000..a688119
--- /dev/null
+++ b/test/portal/portal.js
@@ -0,0 +1 @@
+//JS
diff --git a/test/portal1.ctest.lua b/test/portal1.ctest.lua
new file mode 100644
index 0000000..2bc2122
--- /dev/null
+++ b/test/portal1.ctest.lua
@@ -0,0 +1,37 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local crypto = require("ssso_crypto")
+
+require("do_init")
+
+function test_portal_url_returns_html_with_authorized_links_and_identity()
+ -- given
+ local jws, _ = crypto.get_jws_and_tslimit({
+ u = "guest",
+ p = "",
+ n = "Guest",
+ e = "guest@example.org",
+ })
+ ngx.reset_resp_body()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = jws
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/portal"
+ local expected = [[
+
+
+
+ guest Guest guest@example.org
+
+
+]]
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.say(), expected)
+ lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/portal2.ctest.lua b/test/portal2.ctest.lua
new file mode 100644
index 0000000..82a66cd
--- /dev/null
+++ b/test/portal2.ctest.lua
@@ -0,0 +1,25 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local crypto = require("ssso_crypto")
+
+require("do_init")
+
+function test_portal_css_url_returns_css()
+ -- given
+ local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"})
+ ngx.reset_resp_body()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = jws
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/portal.css"
+ local expected = "/*CSS*/\n"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+ lu.assertEquals(ngx.say(), expected)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/portal3.ctest.lua b/test/portal3.ctest.lua
new file mode 100644
index 0000000..982928a
--- /dev/null
+++ b/test/portal3.ctest.lua
@@ -0,0 +1,25 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local crypto = require("ssso_crypto")
+
+require("do_init")
+
+function test_portal_js_url_returns_js()
+ -- given
+ local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"})
+ ngx.reset_resp_body()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = jws
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/portal.js"
+ local expected = "//JS\n"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 200)
+ lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8")
+ lu.assertEquals(ngx.header["Content-Length"], tostring(#expected))
+ lu.assertEquals(ngx.say(), expected)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/portal4.ctest.lua b/test/portal4.ctest.lua
new file mode 100644
index 0000000..935cff6
--- /dev/null
+++ b/test/portal4.ctest.lua
@@ -0,0 +1,22 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local crypto = require("ssso_crypto")
+
+require("do_init")
+
+function test_unknown_portal_url_returns_404()
+ -- given
+ local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"})
+ ngx.reset_resp_body()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = jws
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/portal.html"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, 404)
+ lu.assertNil(ngx.say())
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/portal5.ctest.lua b/test/portal5.ctest.lua
new file mode 100644
index 0000000..50fb389
--- /dev/null
+++ b/test/portal5.ctest.lua
@@ -0,0 +1,19 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local crypto = require("ssso_crypto")
+
+require("do_init")
+
+function test_portal_url_is_for_authenticated_users_only()
+ -- given
+ ngx.reset_resp_body()
+ ngx.reset_var()
+ ngx.var.request_method = "GET"
+ ngx.var.request_uri = "/ssso/portal"
+ -- when
+ local resp = require("do_access")
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/ssso/portal&cause=401")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/profile.utest.lua b/test/profile.utest.lua
new file mode 100644
index 0000000..577cd79
--- /dev/null
+++ b/test/profile.utest.lua
@@ -0,0 +1,81 @@
+local lu = require("luaunit")
+local prf = require("ssso_profile")
+
+function test_format_replaces_user_placeholders()
+ local profile = {
+ u = "U",
+ }
+ local template = '{user: "\ru.", foo: "bar", name: "\ru."}'
+ lu.assertEquals(prf.format(template, profile), '{user: "U", foo: "bar", name: "U"}')
+end
+
+function test_format_replaces_password_placeholders()
+ local profile = {
+ p = "P",
+ }
+ local template = '{pass: "\rp.", foo: "bar", secret: "\rp."}'
+ lu.assertEquals(prf.format(template, profile), '{pass: "P", foo: "bar", secret: "P"}')
+end
+
+function test_format_replaces_name_placeholders()
+ local profile = {
+ n = "N",
+ }
+ local template = '{name: "\rn.", foo: "bar", nickname: "\rn."}'
+ lu.assertEquals(prf.format(template, profile), '{name: "N", foo: "bar", nickname: "N"}')
+end
+
+function test_format_replaces_email_placeholders()
+ local profile = {
+ e = "user@host",
+ }
+ local template = '{user: "\re.", foo: "bar", mail: "\re."}'
+ lu.assertEquals(prf.format(template, profile), '{user: "user@host", foo: "bar", mail: "user@host"}')
+end
+
+function test_format_replaces_base64_calls()
+ local profile = {
+ u = "👤",
+ p = "🔒",
+ }
+ local template = 'Authorization: Basic \rb64(\ru.:\rp.).'
+ lu.assertEquals(prf.format(template, profile), 'Authorization: Basic 8J+RpDrwn5SS')
+end
+
+function test_format_replaces_base64url_calls()
+ local profile = {
+ u = "👤",
+ p = "🔒",
+ }
+ local template = '?authorization=Basic+\ru64(\ru.:\rp.).'
+ lu.assertEquals(prf.format(template, profile), '?authorization=Basic+8J-RpDrwn5SS')
+end
+
+function test_email_returns_the_profile_s_email()
+ local profile = {
+ e = "E",
+ }
+ lu.assertEquals(prf.email(profile), "E")
+end
+
+
+function test_name_returns_the_profile_s_name()
+ local profile = {
+ n = "N",
+ }
+ lu.assertEquals(prf.name(profile), "N")
+end
+
+
+function test_user_returns_the_profile_s_user()
+ local profile = {
+ u = "U",
+ }
+ lu.assertEquals(prf.user(profile), "U")
+end
+
+function test_build_profile_returns_the_given_information()
+ lu.assertEquals(prf.build_profile("U", "P", "N", "E"), {u = "U", p = "P", n = "N", e = "E"})
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/random.utest.lua b/test/random.utest.lua
new file mode 100644
index 0000000..6b20b49
--- /dev/null
+++ b/test/random.utest.lua
@@ -0,0 +1,12 @@
+local lu = require("luaunit")
+local rnd = require("resty.random")
+
+function test_random()
+ local r1 = rnd.bytes(5, true)
+ local r2 = rnd.bytes(5, false)
+ lu.assertEquals(#r1, 5)
+ lu.assertEquals(#r2, 5)
+ lu.assertNotEquals(r1, r2)
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/sessions.utest.lua b/test/sessions.utest.lua
new file mode 100644
index 0000000..28d1f02
--- /dev/null
+++ b/test/sessions.utest.lua
@@ -0,0 +1,61 @@
+local lu = require("luaunit")
+local sess = require("ssso_sessions")
+local conf = require("ssso_config")
+local crypt = require("ssso_crypto")
+local ngx = require("ngx")
+
+local here = debug.getinfo(1).source:sub(2, -20)
+conf.load_conf(here)
+
+function test_no_session_and_hint_401_if_no_cookie()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ -- when
+ local s, h = sess.get_session()
+ -- then
+ lu.assertNil(s)
+ lu.assertEquals(h, 401)
+end
+
+function test_no_session_and_hint_401_if_empty_cookie()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = ""
+ -- when
+ local s, h = sess.get_session()
+ -- then
+ lu.assertNil(s)
+ lu.assertEquals(h, 401)
+end
+
+function test_no_session_and_hint_403_if_bad_cookie()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.cookie_SSSO_TOKEN = "zzz"
+ -- when
+ local s, h = sess.get_session()
+ -- then
+ lu.assertNil(s)
+ lu.assertEquals(h, 403)
+end
+
+function test_session_and_cookie_renewal_if_good_cookie()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ local data = {u = "bob"}
+ local c, _ = crypt.get_jws_and_tslimit(data)
+ ngx.var.cookie_SSSO_TOKEN = c
+ -- when
+ local s, h = sess.get_session()
+ -- then
+ lu.assertEquals(s, data)
+ lu.assertEquals(h, 200)
+ lu.assertNil(ngx.header["Set-Cookie"].link)
+ lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/sha256.utest.lua b/test/sha256.utest.lua
new file mode 100644
index 0000000..551daf6
--- /dev/null
+++ b/test/sha256.utest.lua
@@ -0,0 +1,13 @@
+local lu = require("luaunit")
+local sha256 = require("resty.sha256")
+
+function test_sha256()
+ local sha1 = sha256:new()
+ sha1:update("test")
+ local sha2 = sha256:new()
+ sha2:update("other")
+ lu.assertEquals(sha1:final(), "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")
+ lu.assertEquals(sha2:final(), "d9298a10d1b0735837dc4bd85dac641b0f3cef27a47e5d53a54f2f3f5b2fcffa")
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/sites.utest.lua b/test/sites.utest.lua
new file mode 100644
index 0000000..23e346f
--- /dev/null
+++ b/test/sites.utest.lua
@@ -0,0 +1,289 @@
+local lu = require("luaunit")
+local ngx = require("ngx")
+local conf = require("ssso_config")
+local ng = require("ssso_nginx")
+local sites = require("ssso_sites")
+
+local here = debug.getinfo(1).source:sub(2, -17)
+conf.load_conf(here)
+sites.load_sites(here)
+
+function test_anonymous_access_to_unknown_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/unknown"
+ local r = ng.get_request()
+ -- when
+ local resp = sites.handle_request(r, nil)
+ -- then
+ lu.assertNil(resp)
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_anonymous_access_to_public_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/public/page"
+ local r = ng.get_request()
+ -- when
+ local resp = sites.handle_request(r, nil)
+ -- then
+ lu.assertNil(resp)
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_anonymous_access_to_public_page_of_mixed_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc"
+ local r = ng.get_request()
+ -- when
+ local resp = sites.handle_request(r, nil)
+ -- then
+ lu.assertNil(resp)
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_anonymous_access_to_private_page_of_mixed_site_redirected_401()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/_new"
+ local r = ng.get_request()
+ -- when
+ local resp = sites.handle_request(r, nil)
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/mixed/bob/wiki/_new&cause=401")
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_anonymous_access_to_private_site_redirected_401()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/private/page"
+ local r = ng.get_request()
+ -- when
+ local resp = sites.handle_request(r, nil)
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=401")
+ lu.assertNil(ngx.req.header["Authorization"])
+end
+
+function test_authenticated_access_to_unknown_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/unknown"
+ local r = ng.get_request()
+ local profile = {
+ u = "U",
+ ok = {},
+ ko = {},
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertNil(resp)
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_authenticated_access_to_public_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/public/page"
+ local r = ng.get_request()
+ local profile = {
+ u = "U",
+ p = "P",
+ ok = {
+ {
+ r = {
+ "^/public",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASS", "\rp."},
+ },
+ },
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertNil(resp)
+ lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASS=P")
+end
+
+
+function test_authenticated_access_to_public_site_can_be_denied()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/public/page"
+ local r = ng.get_request()
+ local profile = {
+ u = "banned",
+ ok = {},
+ ko = {
+ "^/public",
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/public/page&cause=403")
+ lu.assertNil(ngx.req.header["Cookie"])
+end
+
+function test_authenticated_access_to_public_page_of_mixed_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc"
+ local r = ng.get_request()
+ local profile = {
+ u = "U",
+ p = "P",
+ ok = {
+ {
+ r = {
+ "^/public",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASS", "\rp."},
+ },
+ },
+ {
+ r = {
+ "^/mixed/admin",
+ "^/mixed/.-/wiki/_new",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASSWORD", "\rp."},
+ {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."},
+ },
+ },
+ {
+ r = {
+ "^/mixed",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASSWORD", "\rp."},
+ },
+ },
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertNil(resp)
+ lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASSWORD=P")
+ lu.assertNil(ngx.req.header["Authorization"])
+end
+
+function test_authenticated_access_to_private_page_of_mixed_site_accepted()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/mixed/bob/wiki/_new"
+ local r = ng.get_request()
+ local profile = {
+ u = "U",
+ p = "P",
+ ok = {
+ {
+ r = {
+ "^/public",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASS", "\rp."},
+ },
+ },
+ {
+ r = {
+ "^/mixed/admin",
+ "^/mixed/.-/wiki/_new",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASSWORD", "\rp."},
+ {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."},
+ },
+ },
+ {
+ r = {
+ "^/mixed",
+ },
+ a = {
+ {"C", "X-PROXY-USER", "\ru."},
+ {"C", "X-PROXY-PASSWORD", "\rp."},
+ },
+ },
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertNil(resp)
+ lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASSWORD=P")
+ lu.assertEquals(ngx.req.header["Authorization"], "Basic VTpQ")
+end
+
+function test_authenticated_access_to_private_site_accepted_with_the_right_user()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/private/page"
+ local r = ng.get_request()
+ local profile = {
+ u = "jean",
+ p = "P",
+ ok = {
+ {
+ r = {
+ "^/private",
+ },
+ a = {
+ {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."},
+ },
+ },
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertNil(resp)
+ lu.assertEquals(ngx.req.header["Authorization"], "Basic amVhbjpQ")
+end
+
+function test_authenticated_access_to_private_site_redirected_403_with_the_wrong_user()
+ -- given
+ ngx.req.reset()
+ ngx.reset_var()
+ ngx.var.request_uri = "/private/page"
+ local r = ng.get_request()
+ local profile = {
+ u = "U",
+ p = "P",
+ ok = {},
+ ko = {
+ "^/private",
+ }
+ }
+ -- when
+ local resp = sites.handle_request(r, profile)
+ -- then
+ lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=403")
+ lu.assertNil(ngx.req.header["Authorization"])
+end
+
+os.exit(lu.LuaUnit.run())
diff --git a/test/sites/mixed.json b/test/sites/mixed.json
new file mode 100644
index 0000000..fc918dd
--- /dev/null
+++ b/test/sites/mixed.json
@@ -0,0 +1,56 @@
+{
+ "patterns": [
+ {
+ "lua_regex": [
+ "^/mixed/admin",
+ "^/mixed/.-/wiki/_new"
+ ],
+ "public": false,
+ "allow": [
+ "*"
+ ],
+ "deny": [
+ "guest"
+ ],
+ "actions": [
+ {
+ "type": "cookie",
+ "name": "X-PROXY-USER",
+ "value": "\ru."
+ },
+ {
+ "type": "cookie",
+ "name": "X-PROXY-PASSWORD",
+ "value": "\rp."
+ },
+ {
+ "type": "header",
+ "name": "Authorization",
+ "value": "Basic \rb64(\ru.:\rp.)."
+ }
+ ]
+ },
+ {
+ "lua_regex": [
+ "^/mixed"
+ ],
+ "public": true,
+ "actions": [
+ {
+ "type": "cookie",
+ "name": "X-PROXY-USER",
+ "value": "\ru."
+ },
+ {
+ "type": "cookie",
+ "name": "X-PROXY-PASSWORD",
+ "value": "\rp."
+ }
+ ],
+ "portal": {
+ "/mixed/\ru./files": "My files",
+ "/mixed/\ru./webmail": "My e-mail"
+ }
+ }
+ ]
+}
diff --git a/test/sites/private.json b/test/sites/private.json
new file mode 100644
index 0000000..97b3100
--- /dev/null
+++ b/test/sites/private.json
@@ -0,0 +1,25 @@
+{
+ "patterns": [
+ {
+ "lua_regex": [
+ "^/private"
+ ],
+ "public": false,
+ "allow": [
+ "jean"
+ ],
+ "deny": [
+ ],
+ "actions": [
+ {
+ "type": "header",
+ "name": "Authorization",
+ "value": "Basic \rb64(\ru.:\rp.)."
+ }
+ ],
+ "portal": {
+ "/private/directory": "Directory management"
+ }
+ }
+ ]
+}
diff --git a/test/sites/public.json b/test/sites/public.json
new file mode 100644
index 0000000..ceade74
--- /dev/null
+++ b/test/sites/public.json
@@ -0,0 +1,31 @@
+{
+ "patterns": [
+ {
+ "lua_regex": [
+ "^/public"
+ ],
+ "public": true,
+ "allow": [
+ "*"
+ ],
+ "deny": [
+ "banned"
+ ],
+ "actions": [
+ {
+ "type": "cookie",
+ "name": "X-PROXY-USER",
+ "value": "\ru."
+ },
+ {
+ "type": "cookie",
+ "name": "X-PROXY-PASS",
+ "value": "\rp."
+ }
+ ],
+ "portal": {
+ "/public/git/\ru.": "My projects"
+ }
+ }
+ ]
+}
diff --git a/test/util.utest.lua b/test/util.utest.lua
new file mode 100644
index 0000000..d5d6da6
--- /dev/null
+++ b/test/util.utest.lua
@@ -0,0 +1,14 @@
+local lu = require("luaunit")
+local util = require("ssso_util")
+
+function test_str_to_html_replaces_lt_gt_amp_quot_and_percent()
+ lu.assertEquals(util.str_to_html('-&<>"%\r\n=&<>"%+'), '-&<>"%\r\n=&<>"%+')
+end
+
+function test_str_to_pattern_removes_lua_pattern_meanings()
+ lu.assertEquals(
+ util.str_to_pattern("_()%.+-*?^$%a%A%c%C%d%D%l%L%p%P%s%S%u%U%w%W%x%X%z%Z%0%1%2%3%4%5%6%7%8%9%b%fZ\r\n_()%.+-*?^$%a%A%c%C%d%D%l%L%p%P%s%S%u%U%w%W%x%X%z%Z%0%1%2%3%4%5%6%7%8%9%b%fZ"),
+ "_%(%)%%%.%+%-%*%?%^%$%%a%%A%%c%%C%%d%%D%%l%%L%%p%%P%%s%%S%%u%%U%%w%%W%%x%%X%%z%%Z%%0%%1%%2%%3%%4%%5%%6%%7%%8%%9%%b%%fZ\r\n_%(%)%%%.%+%-%*%?%^%$%%a%%A%%c%%C%%d%%D%%l%%L%%p%%P%%s%%S%%u%%U%%w%%W%%x%%X%%z%%Z%%0%%1%%2%%3%%4%%5%%6%%7%%8%%9%%b%%fZ")
+end
+
+os.exit(lu.LuaUnit.run())
From 5f44ced0651f98f0a8c97bc3da4fbec6c968ec33 Mon Sep 17 00:00:00 2001
From: Yves G
Date: Thu, 2 Sep 2021 22:58:01 +0200
Subject: [PATCH 2/5] HTTP basic auth
---
doc/samples/login/login.html | 2 +-
src/ssso_base64.lua | 7 +++-
src/ssso_login.lua | 19 ++++++---
src/ssso_nginx.lua | 35 ++++++++++++++++-
src/ssso_sessions.lua | 19 +++++++--
test/nginx.utest.lua | 76 ++++++++++++++++++++++++++++++++++++
test/sessions.utest.lua | 61 ++++++++++++++++++++++++++++-
7 files changed, 203 insertions(+), 16 deletions(-)
diff --git a/doc/samples/login/login.html b/doc/samples/login/login.html
index 151de65..780cb0f 100644
--- a/doc/samples/login/login.html
+++ b/doc/samples/login/login.html
@@ -6,7 +6,7 @@
Single Sign-On for example.org