diff options
| author | 2014-01-15 05:49:31 (JST) | |
|---|---|---|
| committer | 2014-01-16 10:28:12 (JST) | |
| commit | d6e9200cc35411f3f27426b608bcfdef9348e6d3 (patch) | |
| tree | 9cdd921b03465458d10b99ff4357f79a810501c0 /filters | |
| parent | 3741254a6989b2837cd8d20480f152f0096bcb9a (diff) | |
| download | cgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.zip cgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.tar.gz | |
auth: add basic authentication filter framework
This leverages the new lua support. See
filters/simple-authentication.lua for explaination of how this works.
There is also additional documentation in cgitrc.5.txt.
Though this is a cookie-based approach, cgit's caching mechanism is
preserved for authenticated pages.
Very plugable and extendable depending on user needs.
The sample script uses an HMAC-SHA1 based cookie to store the
currently logged in user, with an expiration date.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'filters')
| -rw-r--r-- | filters/simple-authentication.lua | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua new file mode 100644 index 0000000..4cd4983 --- /dev/null +++ b/filters/simple-authentication.lua | |||
| @@ -0,0 +1,225 @@ | |||
| 1 | -- This script may be used with the auth-filter. Be sure to configure it as you wish. | ||
| 2 | -- | ||
| 3 | -- Requirements: | ||
| 4 | -- luacrypto >= 0.3 | ||
| 5 | -- <http://mkottman.github.io/luacrypto/> | ||
| 6 | -- | ||
| 7 | |||
| 8 | |||
| 9 | -- | ||
| 10 | -- | ||
| 11 | -- Configure these variables for your settings. | ||
| 12 | -- | ||
| 13 | -- | ||
| 14 | |||
| 15 | local protected_repos = { | ||
| 16 | glouglou = { laurent = true, jason = true }, | ||
| 17 | qt = { jason = true, bob = true } | ||
| 18 | } | ||
| 19 | |||
| 20 | local users = { | ||
| 21 | jason = "secretpassword", | ||
| 22 | laurent = "s3cr3t", | ||
| 23 | bob = "ilikelua" | ||
| 24 | } | ||
| 25 | |||
| 26 | local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" | ||
| 27 | |||
| 28 | |||
| 29 | |||
| 30 | -- | ||
| 31 | -- | ||
| 32 | -- Authentication functions follow below. Swap these out if you want different authentication semantics. | ||
| 33 | -- | ||
| 34 | -- | ||
| 35 | |||
| 36 | -- Sets HTTP cookie headers based on post | ||
| 37 | function authenticate_post() | ||
| 38 | local password = users[post["username"]] | ||
| 39 | -- TODO: Implement time invariant string comparison function to mitigate against timing attack. | ||
| 40 | if password == nil or password ~= post["password"] then | ||
| 41 | construct_cookie("", "cgitauth") | ||
| 42 | else | ||
| 43 | construct_cookie(post["username"], "cgitauth") | ||
| 44 | end | ||
| 45 | return 0 | ||
| 46 | end | ||
| 47 | |||
| 48 | |||
| 49 | -- Returns 1 if the cookie is valid and 0 if it is not. | ||
| 50 | function authenticate_cookie() | ||
| 51 | accepted_users = protected_repos[cgit["repo"]] | ||
| 52 | if accepted_users == nil then | ||
| 53 | -- We return as valid if the repo is not protected. | ||
| 54 | return 1 | ||
| 55 | end | ||
| 56 | |||
| 57 | local username = validate_cookie(get_cookie(http["cookie"], "cgitauth")) | ||
| 58 | if username == nil or not accepted_users[username] then | ||
| 59 | return 0 | ||
| 60 | else | ||
| 61 | return 1 | ||
| 62 | end | ||
| 63 | end | ||
| 64 | |||
| 65 | -- Prints the html for the login form. | ||
| 66 | function body() | ||
| 67 | html("<h2>Authentication Required</h2>") | ||
| 68 | html("<form method='post' action='") | ||
| 69 | html_attr(cgit["login"]) | ||
| 70 | html("'>") | ||
| 71 | html("<table>") | ||
| 72 | html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") | ||
| 73 | html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") | ||
| 74 | html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") | ||
| 75 | html("</table></form>") | ||
| 76 | |||
| 77 | return 0 | ||
| 78 | end | ||
| 79 | |||
| 80 | |||
| 81 | -- | ||
| 82 | -- | ||
| 83 | -- Cookie construction and validation helpers. | ||
| 84 | -- | ||
| 85 | -- | ||
| 86 | |||
| 87 | local crypto = require("crypto") | ||
| 88 | |||
| 89 | -- Returns username of cookie if cookie is valid. Otherwise returns nil. | ||
| 90 | function validate_cookie(cookie) | ||
| 91 | local i = 0 | ||
| 92 | local username = "" | ||
| 93 | local expiration = 0 | ||
| 94 | local salt = "" | ||
| 95 | local hmac = "" | ||
| 96 | |||
| 97 | if cookie:len() < 3 or cookie:sub(1, 1) == "|" then | ||
| 98 | return nil | ||
| 99 | end | ||
| 100 | |||
| 101 | for component in string.gmatch(cookie, "[^|]+") do | ||
| 102 | if i == 0 then | ||
| 103 | username = component | ||
| 104 | elseif i == 1 then | ||
| 105 | expiration = tonumber(component) | ||
| 106 | if expiration == nil then | ||
| 107 | expiration = 0 | ||
| 108 | end | ||
| 109 | elseif i == 2 then | ||
| 110 | salt = component | ||
| 111 | elseif i == 3 then | ||
| 112 | hmac = component | ||
| 113 | else | ||
| 114 | break | ||
| 115 | end | ||
| 116 | i = i + 1 | ||
| 117 | end | ||
| 118 | |||
| 119 | if hmac == nil or hmac:len() == 0 then | ||
| 120 | return nil | ||
| 121 | end | ||
| 122 | |||
| 123 | -- TODO: implement time invariant comparison to prevent against timing attack. | ||
| 124 | if hmac ~= crypto.hmac.digest("sha1", username .. "|" .. tostring(expiration) .. "|" .. salt, secret) then | ||
| 125 | return nil | ||
| 126 | end | ||
| 127 | |||
| 128 | if expiration <= os.time() then | ||
| 129 | return nil | ||
| 130 | end | ||
| 131 | |||
| 132 | return username:lower() | ||
| 133 | end | ||
| 134 | |||
| 135 | function construct_cookie(username, cookie) | ||
| 136 | local authstr = "" | ||
| 137 | if username:len() > 0 then | ||
| 138 | -- One week expiration time | ||
| 139 | local expiration = os.time() + 604800 | ||
| 140 | local salt = crypto.hex(crypto.rand.bytes(16)) | ||
| 141 | |||
| 142 | authstr = username .. "|" .. tostring(expiration) .. "|" .. salt | ||
| 143 | authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) | ||
| 144 | end | ||
| 145 | |||
| 146 | html("Set-Cookie: " .. cookie .. "=" .. authstr .. "; HttpOnly") | ||
| 147 | if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then | ||
| 148 | html("; secure") | ||
| 149 | end | ||
| 150 | html("\n") | ||
| 151 | end | ||
| 152 | |||
| 153 | -- | ||
| 154 | -- | ||
| 155 | -- Wrapper around filter API follows below, exposing the http table, the cgit table, and the post table to the above functions. | ||
| 156 | -- | ||
| 157 | -- | ||
| 158 | |||
| 159 | local actions = {} | ||
| 160 | actions["authenticate-post"] = authenticate_post | ||
| 161 | actions["authenticate-cookie"] = authenticate_cookie | ||
| 162 | actions["body"] = body | ||
| 163 | |||
| 164 | function filter_open(...) | ||
| 165 | action = actions[select(1, ...)] | ||
| 166 | |||
| 167 | http = {} | ||
| 168 | http["cookie"] = select(2, ...) | ||
| 169 | http["method"] = select(3, ...) | ||
| 170 | http["query"] = select(4, ...) | ||
| 171 | http["referer"] = select(5, ...) | ||
| 172 | http["path"] = select(6, ...) | ||
| 173 | http["host"] = select(7, ...) | ||
| 174 | http["https"] = select(8, ...) | ||
| 175 | |||
| 176 | cgit = {} | ||
| 177 | cgit["repo"] = select(9, ...) | ||
| 178 | cgit["page"] = select(10, ...) | ||
| 179 | cgit["url"] = select(11, ...) | ||
| 180 | |||
| 181 | cgit["login"] = "" | ||
| 182 | for _ in cgit["url"]:gfind("/") do | ||
| 183 | cgit["login"] = cgit["login"] .. "../" | ||
| 184 | end | ||
| 185 | cgit["login"] = cgit["login"] .. "?p=login" | ||
| 186 | |||
| 187 | end | ||
| 188 | |||
| 189 | function filter_close() | ||
| 190 | return action() | ||
| 191 | end | ||
| 192 | |||
| 193 | function filter_write(str) | ||
| 194 | post = parse_qs(str) | ||
| 195 | end | ||
| 196 | |||
| 197 | |||
| 198 | -- | ||
| 199 | -- | ||
| 200 | -- Utility functions follow below, based on keplerproject/wsapi. | ||
| 201 | -- | ||
| 202 | -- | ||
| 203 | |||
| 204 | function url_decode(str) | ||
| 205 | if not str then | ||
| 206 | return "" | ||
| 207 | end | ||
| 208 | str = string.gsub(str, "+", " ") | ||
| 209 | str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) | ||
| 210 | str = string.gsub(str, "\r\n", "\n") | ||
| 211 | return str | ||
| 212 | end | ||
| 213 | |||
| 214 | function parse_qs(qs) | ||
| 215 | local tab = {} | ||
| 216 | for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do | ||
| 217 | tab[url_decode(key)] = url_decode(val) | ||
| 218 | end | ||
| 219 | return tab | ||
| 220 | end | ||
| 221 | |||
| 222 | function get_cookie(cookies, name) | ||
| 223 | cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") | ||
| 224 | return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) | ||
| 225 | end | ||
