Diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/extensions/test-auth.lua | 147 |
1 file changed, 65 insertions, 82 deletions
diff --git a/tests/extensions/test-auth.lua b/tests/extensions/test-auth.lua index 742eabe..974544d 100644 --- a/tests/extensions/test-auth.lua +++ b/tests/extensions/test-auth.lua @@ -94,7 +94,6 @@ h.equals("an account is found whatever its case", hash, stored_hash) h.equals("an unknown account is not", account_hash("nobody"), nil) local _, stand_in = account_hash("nobody") h.equals("it brings another account's hash to spend instead", stand_in, stored_hash) -h.equals("a nil user is not", account_hash(nil), nil) local userset = repo_userset("secret-repo") h.check("a protected repository lists its users", @@ -113,12 +112,14 @@ if variant == "file" then h.equals("a missing users file turns every login down", account_hash("alice"), nil) h.redirect_file("/etc/cgit-auth/users", "auth-users") -- Without the repos file nothing tells a protected repository from a - -- public one, so every repository is refused while the index stays up. + -- public one, so every page is refused, the index too, which cgit asks + -- about with an empty repository name. h.redirect_file("/etc/cgit-auth/repos", "auth-repos-missing") local refused = repo_userset("unlisted") h.check("a missing repos file refuses every repository", refused ~= nil and next(refused) == nil) - h.equals("the index stays public all the same", repo_userset(nil), nil) + local index = repo_userset("") + h.check("and the index as well", index ~= nil and next(index) == nil) h.redirect_file("/etc/cgit-auth/repos", "auth-repos") end @@ -141,7 +142,7 @@ h.equals("no header yields no cookie", get_cookie(nil, "cgitauth"), nil) h.equals("a longer name does not match", get_cookie("xcgitauth=z", "cgitauth"), nil) h.equals("a magic character in the name is taken literally", get_cookie("a-b=z", "a-b"), "z") h.equals("spaces around a pair are ignored", get_cookie(" foo=1 ; cgitauth=abc ", "cgitauth"), "abc") -h.equals("a value keeps its own equals signs", get_cookie("cgitauth=a=b", "cgitauth"), "a=b") +h.equals("a cookie value keeps its own equals signs", get_cookie("cgitauth=a=b", "cgitauth"), "a=b") -- The Cookie header is the visitor's to write, so a run of spaces in it -- must not cost the square of its length. @@ -161,8 +162,6 @@ h.check("a scheme relative target is not", not is_safe_redirect("//evil")) h.check("a backslash variant is not", not is_safe_redirect("/\\evil")) h.check("a missing target is not", not is_safe_redirect(nil)) -h.equals("control characters are stripped from header values", strip_controls("a\r\nb"), "ab") - -- Signing and verification. local now = os.time() @@ -192,6 +191,7 @@ h.equals("a value signed for one field does not serve another", validate_value(" h.equals("no cookie does not verify", validate_value("username", nil), nil) h.equals("a tiny cookie does not verify", validate_value("username", "ab"), nil) h.equals("a leading separator does not verify", validate_value("username", "|x|1|s|sig"), nil) +h.equals("a trailing field does not verify", validate_value("username", cookie .. "|x"), nil) h.equals("an unsigned cookie does not verify", validate_value("username", "username|alice|123|salt"), nil) h.equals("an exponent spelling of the expiry does not verify", @@ -237,106 +237,96 @@ local function run_action(action, opts) return h.output(), ret end -local ret +-- A login posts the form, and a visit asks about a repository, with a +-- session cookie when it is given one, and comes back with the answer. +local function login(user, pass, redirect) + local body = "username=" .. url_encode(user) .. "&password=" .. url_encode(pass) + if redirect then + body = body .. "&redirect=" .. url_encode(redirect) + end + return run_action("authenticate-post", { method = "POST", body = body }) +end -out, ret = run_action("authenticate-cookie", { repo = "unlisted" }) -h.equals("a public repository needs no cookie", ret, 1) +local function visit(repo, token) + local _, ret = run_action("authenticate-cookie", + { repo = repo, cookie = token and session_name .. "=" .. token }) + return ret +end -out, ret = run_action("authenticate-cookie", { repo = "secret-repo" }) -h.equals("a protected repository turns a bare request away", ret, 0) +-- Where the signed way back in a login form leads. +local function way_back_in(form) + local token = form:match("name='redirect' value='([^']*)'") + return token and validate_value("redirect", token) +end + +h.equals("a public repository needs no cookie", visit("unlisted"), 1) +h.equals("a protected repository turns a bare request away", visit("secret-repo"), 0) local session = secure_value("username", "Alice", now + 3600, stored_hash) -out, ret = run_action("authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. session }) -h.equals("a signed session for a member is let through", ret, 1) +h.equals("a signed session for a member is let through", visit("secret-repo", session), 1) -- A session is signed with its account's hash, so a new password or a -- removed account ends it. local stale = secure_value("username", "Alice", now + 3600, "$6$rounds=300000$old$hash") -out, ret = run_action("authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. stale }) -h.equals("a session from before a password change is turned away", ret, 0) +h.equals("a session from before a password change is turned away", visit("secret-repo", stale), 0) local orphan = secure_value("username", "bob", now + 3600, stored_hash) -out, ret = run_action("authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. orphan }) -h.equals("a session for a member with no account is turned away", ret, 0) +h.equals("a session for a member with no account is turned away", visit("secret-repo", orphan), 0) local unbound = secure_value("username", "Alice", now + 3600) h.equals("a session signed without its binding does not verify", validate_value("username", unbound, account_hash), nil) -out, ret = run_action("authenticate-cookie", { repo = "empty-repo", cookie = session_name .. "=" .. session }) -h.equals("a memberless repository denies even a valid session", ret, 0) +h.equals("a memberless repository denies even a valid session", visit("empty-repo", session), 0) local outsider = secure_value("username", "carol", now + 3600, stored_hash) -out, ret = run_action("authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. outsider }) -h.equals("a signed session for an outsider is turned away", ret, 0) +h.equals("a signed session for an outsider is turned away", visit("secret-repo", outsider), 0) local forged = session:sub(1, -2) .. (session:sub(-1) == "0" and "1" or "0") -out, ret = run_action("authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. forged }) -h.equals("a forged session is turned away", ret, 0) +h.equals("a forged session is turned away", visit("secret-repo", forged), 0) -out, ret = run_action("no-such-action", {}) +local _, ret = run_action("no-such-action", {}) h.equals("an unknown action denies rather than raising", ret, 0) -out, ret = run_action("body", { url = "/repo/log/?q=x" }) +out = run_action("body", { url = "/repo/log/?q=x" }) h.contains("the login form posts to the login url", out, "<form class='login' method='post' action='/?p=login'>") h.equals("a first visit says nothing failed", out:find("login-error", 1, true), nil) -local token = out:match("name='redirect' value='([^']*)'") -h.equals("the form carries a signed way back", token and validate_value("redirect", token), "/repo/log/?q=x") +h.equals("the form carries a signed way back", way_back_in(out), "/repo/log/?q=x") -out, ret = run_action("body", { url = "//evil.example/x" }) -token = out:match("name='redirect' value='([^']*)'") -h.equals("an unsafe destination is swapped for the login page", - token and validate_value("redirect", token), "/?p=login") +out = run_action("body", { url = "//evil.example/x" }) +h.equals("an unsafe destination is swapped for the login page", way_back_in(out), "/?p=login") h.equals("and is not taken for a failed login", out:find("login-error", 1, true), nil) -out, ret = run_action("body", { url = "/repo/log/?q=x&login=failed" }) +out = run_action("body", { url = "/repo/log/?q=x&login=failed" }) h.contains("a failed login says so", out, "<p class='login-error' role='alert'>Wrong username or password.</p>") -token = out:match("name='redirect' value='([^']*)'") -h.equals("and leaves the flag out of the way back", token and validate_value("redirect", token), "/repo/log/?q=x") +h.equals("and leaves the flag out of the way back", way_back_in(out), "/repo/log/?q=x") local way_back = secure_value("redirect", "/repo/", 0) -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. url_encode(password) .. "&redirect=" .. url_encode(way_back), -}) +out = login("Alice", password, way_back) h.contains("a good login redirects back", out, "Status: 302") h.contains("to where the form said", out, "Location: /repo/\n") local granted = out:match("Set%-Cookie: __Host%-cgitauth=([^;]*);") h.equals("and grants a session that verifies", granted and validate_value("username", granted, account_hash), "Alice") -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=wrong&redirect=" .. url_encode(way_back), -}) +out = login("Alice", "wrong", way_back) h.contains("a bad password redirects the same way", out, "Status: 302") h.contains("back to the page with the failure flagged", out, "Location: /repo/?login=failed\n") h.contains("but clears the session cookie", out, "Set-Cookie: " .. session_name .. "=; ") -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=nobody&password=x&redirect=" .. url_encode(way_back), -}) -h.contains("an unknown user is told nothing different", out, "Set-Cookie: " .. session_name .. "=; ") -h.contains("and lands on the same flagged page", out, "Location: /repo/?login=failed\n") - -- A failed login for a name with no account spends crypt on a configured -- hash, the same work a real check costs, so timing tells nothing. crypt_settings = {} -run_action("authenticate-post", { - method = "POST", - body = "username=nobody&password=x&redirect=" .. url_encode(way_back), -}) +out = login("nobody", "x", way_back) +h.contains("an unknown user is told nothing different", out, "Set-Cookie: " .. session_name .. "=; ") +h.contains("and lands on the same flagged page", out, "Location: /repo/?login=failed\n") h.equals("a missing account spends crypt on a configured hash", crypt_settings[1], stored_hash) if variant == "file" then h.redirect_file("/etc/cgit-auth/users", "auth-users-missing") crypt_settings = {} - run_action("authenticate-post", { - method = "POST", - body = "username=nobody&password=x&redirect=" .. url_encode(way_back), - }) + login("nobody", "x", way_back) h.equals("with no accounts at all nothing is spent", #crypt_settings, 0) h.redirect_file("/etc/cgit-auth/users", "auth-users") end @@ -344,41 +334,36 @@ end -- crypt's work grows with the password, so one past the cap is turned down -- before crypt sees it. crypt_settings = {} -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. string.rep("a", 257) .. "&redirect=" .. url_encode(way_back), -}) +out = login("Alice", string.rep("a", 257), way_back) h.contains("an overlong password is turned down", out, "Set-Cookie: " .. session_name .. "=; ") h.equals("without spending crypt on it", #crypt_settings, 0) -run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. string.rep("a", 256) .. "&redirect=" .. url_encode(way_back), -}) +login("Alice", string.rep("a", 256), way_back) h.equals("a password at the cap is still checked", #crypt_settings, 1) -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. url_encode(password), -}) +out = login("Alice", password) h.contains("a post without the signed token is not served", out, "Status: 404") -local hijack = secure_value("redirect", "//evil.example/", 0) -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. url_encode(password) .. "&redirect=" .. url_encode(hijack), -}) +out = login("Alice", password, secure_value("redirect", "//evil.example/", 0)) h.contains("even a signed unsafe destination is not followed", out, "Status: 404") -- Over plain http the cookie cannot be Secure, and so loses the __Host- name. load_script({ { "local cookie_insecure = false", "local cookie_insecure = true" } }) -out, ret = run_action("authenticate-post", { - method = "POST", - body = "username=Alice&password=" .. url_encode(password) .. "&redirect=" .. url_encode(way_back), -}) +out = login("Alice", password, way_back) h.contains("an insecure session cookie keeps its plain name", out, "Set-Cookie: cgitauth=") h.excludes("and is not marked Secure", out, "; Secure") load_script() +-- A cookie carries whole seconds, while on Lua 5.3 and later a session length +-- worked out with a fraction is a float, which tostring writes with a ".0". +load_script({ { "local session_seconds = 7 * 24 * 60 * 60", + "local session_seconds = 1.5 * 24 * 60 * 60" } }) +out = login("Alice", password, way_back) +granted = out:match("Set%-Cookie: __Host%-cgitauth=([^;]*);") +h.equals("a session a day and a half long still verifies", + granted and validate_value("username", granted, account_hash), "Alice") +h.contains("and its cookie lasts as many whole seconds", out, "; Max-Age=129600\n") +load_script() + -- The libraries load only once a request needs one, so with every one of -- them gone a public repository is still served, while a protected one -- fails closed instead of reaching its content. @@ -386,10 +371,8 @@ for _, name in ipairs({ "openssl.rand", "openssl.hmac", "posix.sys.stat", "posix package.loaded[name] = nil h.preload_failure(name) end -out, ret = run_action("authenticate-cookie", { repo = "unlisted" }) -h.equals("without the libraries a public repository is served", ret, 1) -h.check("while a protected one fails closed", not pcall(run_action, - "authenticate-cookie", { repo = "secret-repo", cookie = session_name .. "=" .. session })) +h.equals("without the libraries a public repository is served", visit("unlisted"), 1) +h.check("while a protected one fails closed", not pcall(visit, "secret-repo", session)) h.check("and so does its login form", not pcall(run_action, "body", {})) h.finish() |
