From 5f44ced0651f98f0a8c97bc3da4fbec6c968ec33 Mon Sep 17 00:00:00 2001 From: Yves G Date: Thu, 2 Sep 2021 22:58:01 +0200 Subject: [PATCH] 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

- + diff --git a/src/ssso_base64.lua b/src/ssso_base64.lua index 6179da1..d13eade 100644 --- a/src/ssso_base64.lua +++ b/src/ssso_base64.lua @@ -10,9 +10,12 @@ end if not b64["encode_base64"] then b64.encode_base64 = function(plaintext) - local plain = b64.encode_base64url(plaintext) + local plain, err = b64.encode_base64url(plaintext) + if not plain then + return nil, err + end plain = plain:gsub("_", "/") - return plain:gsub("%-", "+") + return plain:gsub("%-", "+"), nil end end diff --git a/src/ssso_login.lua b/src/ssso_login.lua index e016810..1f4169d 100644 --- a/src/ssso_login.lua +++ b/src/ssso_login.lua @@ -47,15 +47,23 @@ local function inject_data(html, req_data, warnings) 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 function check_credentials_and_get_profile(user, password) 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) + return sites.with_sites(profile, user) + else + return nil + end +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 profile = check_credentials_and_get_profile(user, password) + if profile then log.debug("Building JWS") local jws, tslimit = crypto.get_jws_and_tslimit(profile) if not jws then @@ -100,5 +108,6 @@ end return { answer_request = answer_request, + check_credentials_and_get_profile = check_credentials_and_get_profile, set_root = set_root, } diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua index 889c726..8be3dc1 100644 --- a/src/ssso_nginx.lua +++ b/src/ssso_nginx.lua @@ -1,4 +1,5 @@ local ngx = require("ngx") +local b64 = require("ssso_base64") local util = require("ssso_util") local conf = require("ssso_config") @@ -43,6 +44,36 @@ local function with_post_parameters(req_data) return req_data end +local function str_starts_with(str, begin) + local i, _ = str:find(util.str_to_pattern(begin)) + return 1 == i +end + +local function get_basic_auth() + local header = ngx.var.Authentication + if (not header) or (#header < 7) or (not str_starts_with(header, "Basic ")) then + return nil, nil + end + local text, _ = b64.decode_base64(header:sub(7)) + if not text then + ngx.log(ngx.DEBUG, "Invalid Authentication header: " .. header) + return nil, nil + end + local colon + colon, _ = text:find(":") + if not colon then + return nil, nil + end + local login, password = "", "" + if colon > 1 then + login = text:sub(1, colon - 1) + end + if colon < #text then + password = text:sub(colon + 1, #text) + end + return login, password +end + local function get_jws_cookie() return ngx.var.cookie_SSSO_TOKEN end @@ -85,8 +116,7 @@ local function matches(req_data, lua_pattern) end local function starts_with(req_data, prefix) - local i, _ = req_data.target:find(util.str_to_pattern(prefix)) - return 1 == i + return str_starts_with(req_data.target, prefix) end local function has_param(req_data, param, value) @@ -138,6 +168,7 @@ return { add_header = add_header, answer_not_found = answer_not_found, forward_request = forward_request, + get_basic_auth = get_basic_auth, get_jws_cookie = get_jws_cookie, get_request = get_request, get_seconds_since_epoch = get_seconds_since_epoch, diff --git a/src/ssso_sessions.lua b/src/ssso_sessions.lua index b681943..c910930 100644 --- a/src/ssso_sessions.lua +++ b/src/ssso_sessions.lua @@ -1,14 +1,25 @@ local crypto = require("ssso_crypto") +local login = require("ssso_login") local nginx = require("ssso_nginx") local function get_session() - local cookie = nginx.get_jws_cookie() + local session, jws, tslimit + local user, password = nginx.get_basic_auth() - if not cookie or cookie == "" then - return nil, 401 + if user and password then + session = login.check_credentials_and_get_profile(user, password) + if session then + jws, tslimit = crypto.get_jws_and_tslimit(session) + end end - local session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + if not session then + local cookie = nginx.get_jws_cookie() + if not cookie or cookie == "" then + return nil, 401 + end + session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + end if session then nginx.set_jws_cookie(jws, tslimit) diff --git a/test/nginx.utest.lua b/test/nginx.utest.lua index fbbe1d5..26d7ce2 100644 --- a/test/nginx.utest.lua +++ b/test/nginx.utest.lua @@ -310,4 +310,80 @@ function test_with_post_parameters_merges_post_parameters_to_request_data() }) end +function test_get_basic_auth_with_no_header_returns_nil() + -- given + ngx.reset_header() + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_non_basic_header_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Bearer uuid" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_basic_header_but_no_base64_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic " + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_basic_header_but_invalid_base64_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic !!!!" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_valid_basic_header_returns_auth() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic dTpw" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "u") + lu.assertEquals(p, "p") +end + +function test_get_basic_auth_works_with_an_empty_login() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic OnA=" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "") + lu.assertEquals(p, "p") +end + +function test_get_basic_auth_works_with_an_empty_password() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic dTo=" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "u") + lu.assertEquals(p, "") +end + os.exit(lu.LuaUnit.run()) diff --git a/test/sessions.utest.lua b/test/sessions.utest.lua index 28d1f02..3e34dc9 100644 --- a/test/sessions.utest.lua +++ b/test/sessions.utest.lua @@ -1,11 +1,15 @@ local lu = require("luaunit") -local sess = require("ssso_sessions") +local ngx = require("ngx") +local b64 = require("ssso_base64") local conf = require("ssso_config") local crypt = require("ssso_crypto") -local ngx = require("ngx") +local login = require("ssso_login") +local sess = require("ssso_sessions") +local sites = require("ssso_sites") local here = debug.getinfo(1).source:sub(2, -20) conf.load_conf(here) +sites.load_sites(here) function test_no_session_and_hint_401_if_no_cookie() -- given @@ -45,6 +49,7 @@ end function test_session_and_cookie_renewal_if_good_cookie() -- given ngx.req.reset() + ngx.reset_header() ngx.reset_var() local data = {u = "bob"} local c, _ = crypt.get_jws_and_tslimit(data) @@ -58,4 +63,56 @@ function test_session_and_cookie_renewal_if_good_cookie() lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") end +function test_good_basic_auth_credentials_generate_a_session_and_a_cookie() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + ngx.var.Authentication = "Basic " .. b64.encode_base64("bob:goodpassword") + local expected = login.check_credentials_and_get_profile("bob", "goodpassword") + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s, expected) + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +function test_basic_auth_takes_precedence_over_cookie() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + local data = {u = "forget me"} + local c, _ = crypt.get_jws_and_tslimit(data) + ngx.var.cookie_SSSO_TOKEN = c + ngx.var.Authentication = "Basic " .. b64.encode_base64("bob:goodpassword") + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s.u, "bob") + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +function test_basic_auth_ignored_if_invalid() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + local data = {u = "do not forget me"} + local c, _ = crypt.get_jws_and_tslimit(data) + ngx.var.cookie_SSSO_TOKEN = c + ngx.var.Authentication = "Basic !!!!" + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s.u, "do not forget me") + 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())