]> gitweb.ps.run Git - ps-cgit/blob - filters/simple-authentication.lua
bf356324e2b00865d727cb3e9065b094286c0a73
[ps-cgit] / filters / simple-authentication.lua
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 --      luaposix
7 --      <https://github.com/luaposix/luaposix>
8 --
9 local sysstat = require("posix.sys.stat")
10 local unistd = require("posix.unistd")
11 local crypto = require("crypto")
12
13
14 --
15 --
16 -- Configure these variables for your settings.
17 --
18 --
19
20 -- A list of password protected repositories along with the users who can access them.
21 local protected_repos = {
22         glouglou        = { laurent = true, jason = true },
23         qt              = { jason = true, bob = true }
24 }
25
26 -- Please note that, in production, you'll want to replace this simple lookup
27 -- table with either a table of salted and hashed passwords (using something
28 -- smart like scrypt), or replace this table lookup with an external support,
29 -- such as consulting your system's pam / shadow system, or an external
30 -- database, or an external validating web service. For testing, or for
31 -- extremely low-security usage, you may be able, however, to get away with
32 -- compromising on hardcoding the passwords in cleartext, as we have done here.
33 local users = {
34         jason           = "secretpassword",
35         laurent         = "s3cr3t",
36         bob             = "ilikelua"
37 }
38
39 -- Set this to a path this script can write to for storing a persistent
40 -- cookie secret, which should be guarded.
41 local secret_filename = "/var/cache/cgit/auth-secret"
42
43 --
44 --
45 -- Authentication functions follow below. Swap these out if you want different authentication semantics.
46 --
47 --
48
49 -- Sets HTTP cookie headers based on post and sets up redirection.
50 function authenticate_post()
51         local password = users[post["username"]]
52         local redirect = validate_value("redirect", post["redirect"])
53
54         if redirect == nil then
55                 not_found()
56                 return 0
57         end
58
59         redirect_to(redirect)
60
61         -- Lua hashes strings, so these comparisons are time invariant.
62         if password == nil or password ~= post["password"] then
63                 set_cookie("cgitauth", "")
64         else
65                 -- One week expiration time
66                 local username = secure_value("username", post["username"], os.time() + 604800)
67                 set_cookie("cgitauth", username)
68         end
69
70         html("\n")
71         return 0
72 end
73
74
75 -- Returns 1 if the cookie is valid and 0 if it is not.
76 function authenticate_cookie()
77         accepted_users = protected_repos[cgit["repo"]]
78         if accepted_users == nil then
79                 -- We return as valid if the repo is not protected.
80                 return 1
81         end
82
83         local username = validate_value("username", get_cookie(http["cookie"], "cgitauth"))
84         if username == nil or not accepted_users[username:lower()] then
85                 return 0
86         else
87                 return 1
88         end
89 end
90
91 -- Prints the html for the login form.
92 function body()
93         html("<h2>Authentication Required</h2>")
94         html("<form method='post' action='")
95         html_attr(cgit["login"])
96         html("'>")
97         html("<input type='hidden' name='redirect' value='")
98         html_attr(secure_value("redirect", cgit["url"], 0))
99         html("' />")
100         html("<table>")
101         html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>")
102         html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>")
103         html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>")
104         html("</table></form>")
105
106         return 0
107 end
108
109
110
111 --
112 --
113 -- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions.
114 --
115 --
116
117 local actions = {}
118 actions["authenticate-post"] = authenticate_post
119 actions["authenticate-cookie"] = authenticate_cookie
120 actions["body"] = body
121
122 function filter_open(...)
123         action = actions[select(1, ...)]
124
125         http = {}
126         http["cookie"] = select(2, ...)
127         http["method"] = select(3, ...)
128         http["query"] = select(4, ...)
129         http["referer"] = select(5, ...)
130         http["path"] = select(6, ...)
131         http["host"] = select(7, ...)
132         http["https"] = select(8, ...)
133
134         cgit = {}
135         cgit["repo"] = select(9, ...)
136         cgit["page"] = select(10, ...)
137         cgit["url"] = select(11, ...)
138         cgit["login"] = select(12, ...)
139
140 end
141
142 function filter_close()
143         return action()
144 end
145
146 function filter_write(str)
147         post = parse_qs(str)
148 end
149
150
151 --
152 --
153 -- Utility functions based on keplerproject/wsapi.
154 --
155 --
156
157 function url_decode(str)
158         if not str then
159                 return ""
160         end
161         str = string.gsub(str, "+", " ")
162         str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
163         str = string.gsub(str, "\r\n", "\n")
164         return str
165 end
166
167 function url_encode(str)
168         if not str then
169                 return ""
170         end
171         str = string.gsub(str, "\n", "\r\n")
172         str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end)
173         str = string.gsub(str, " ", "+")
174         return str
175 end
176
177 function parse_qs(qs)
178         local tab = {}
179         for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
180                 tab[url_decode(key)] = url_decode(val)
181         end
182         return tab
183 end
184
185 function get_cookie(cookies, name)
186         cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";")
187         return url_decode(string.match(cookies, ";" .. name .. "=(.-);"))
188 end
189
190
191 --
192 --
193 -- Cookie construction and validation helpers.
194 --
195 --
196
197 local secret = nil
198
199 -- Loads a secret from a file, creates a secret, or returns one from memory.
200 function get_secret()
201         if secret ~= nil then
202                 return secret
203         end
204         local secret_file = io.open(secret_filename, "r")
205         if secret_file == nil then
206                 local old_umask = sysstat.umask(63)
207                 local temporary_filename = secret_filename .. ".tmp." .. crypto.hex(crypto.rand.bytes(16))
208                 local temporary_file = io.open(temporary_filename, "w")
209                 if temporary_file == nil then
210                         os.exit(177)
211                 end
212                 temporary_file:write(crypto.hex(crypto.rand.bytes(32)))
213                 temporary_file:close()
214                 unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same.
215                 unistd.unlink(temporary_filename)
216                 sysstat.umask(old_umask)
217                 secret_file = io.open(secret_filename, "r")
218         end
219         if secret_file == nil then
220                 os.exit(177)
221         end
222         secret = secret_file:read()
223         secret_file:close()
224         if secret:len() ~= 64 then
225                 os.exit(177)
226         end
227         return secret
228 end
229
230 -- Returns value of cookie if cookie is valid. Otherwise returns nil.
231 function validate_value(expected_field, cookie)
232         local i = 0
233         local value = ""
234         local field = ""
235         local expiration = 0
236         local salt = ""
237         local hmac = ""
238
239         if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then
240                 return nil
241         end
242
243         for component in string.gmatch(cookie, "[^|]+") do
244                 if i == 0 then
245                         field = component
246                 elseif i == 1 then
247                         value = component
248                 elseif i == 2 then
249                         expiration = tonumber(component)
250                         if expiration == nil then
251                                 expiration = -1
252                         end
253                 elseif i == 3 then
254                         salt = component
255                 elseif i == 4 then
256                         hmac = component
257                 else
258                         break
259                 end
260                 i = i + 1
261         end
262
263         if hmac == nil or hmac:len() == 0 then
264                 return nil
265         end
266
267         -- Lua hashes strings, so these comparisons are time invariant.
268         if hmac ~= crypto.hmac.digest("sha256", field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt, get_secret()) then
269                 return nil
270         end
271
272         if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then
273                 return nil
274         end
275
276         if url_decode(field) ~= expected_field then
277                 return nil
278         end
279
280         return url_decode(value)
281 end
282
283 function secure_value(field, value, expiration)
284         if value == nil or value:len() <= 0 then
285                 return ""
286         end
287
288         local authstr = ""
289         local salt = crypto.hex(crypto.rand.bytes(16))
290         value = url_encode(value)
291         field = url_encode(field)
292         authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
293         authstr = authstr .. "|" .. crypto.hmac.digest("sha256", authstr, get_secret())
294         return authstr
295 end
296
297 function set_cookie(cookie, value)
298         html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly")
299         if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then
300                 html("; secure")
301         end
302         html("\n")
303 end
304
305 function redirect_to(url)
306         html("Status: 302 Redirect\n")
307         html("Cache-Control: no-cache, no-store\n")
308         html("Location: " .. url .. "\n")
309 end
310
311 function not_found()
312         html("Status: 404 Not Found\n")
313         html("Cache-Control: no-cache, no-store\n\n")
314 end