]> gitweb.ps.run Git - ps-cgit/blob - filters/simple-authentication.lua
git: update to v2.46.0
[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 --      luaossl
5 --      <http://25thandclement.com/~william/projects/luaossl.html>
6 --      luaposix
7 --      <https://github.com/luaposix/luaposix>
8 --
9 local sysstat = require("posix.sys.stat")
10 local unistd = require("posix.unistd")
11 local rand = require("openssl.rand")
12 local hmac = require("openssl.hmac")
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 -- A list of users and hashes, generated with `mkpasswd -m sha-512 -R 300000`.
27 local users = {
28         jason           = "$6$rounds=300000$YYJct3n/o.ruYK$HhpSeuCuW1fJkpvMZOZzVizeLsBKcGA/aF2UPuV5v60JyH2MVSG6P511UMTj2F3H75.IT2HIlnvXzNb60FcZH1",
29         laurent         = "$6$rounds=300000$dP0KNHwYb3JKigT$pN/LG7rWxQ4HniFtx5wKyJXBJUKP7R01zTNZ0qSK/aivw8ywGAOdfYiIQFqFhZFtVGvr11/7an.nesvm8iJUi.",
30         bob             = "$6$rounds=300000$jCLCCt6LUpTz$PI1vvd1yaVYcCzqH8QAJFcJ60b6W/6sjcOsU7mAkNo7IE8FRGW1vkjF8I/T5jt/auv5ODLb1L4S2s.CAyZyUC"
31 }
32
33 -- Set this to a path this script can write to for storing a persistent
34 -- cookie secret, which should be guarded.
35 local secret_filename = "/var/cache/cgit/auth-secret"
36
37 --
38 --
39 -- Authentication functions follow below. Swap these out if you want different authentication semantics.
40 --
41 --
42
43 -- Sets HTTP cookie headers based on post and sets up redirection.
44 function authenticate_post()
45         local hash = users[post["username"]]
46         local redirect = validate_value("redirect", post["redirect"])
47
48         if redirect == nil then
49                 not_found()
50                 return 0
51         end
52
53         redirect_to(redirect)
54
55         if hash == nil or hash ~= unistd.crypt(post["password"], hash) then
56                 set_cookie("cgitauth", "")
57         else
58                 -- One week expiration time
59                 local username = secure_value("username", post["username"], os.time() + 604800)
60                 set_cookie("cgitauth", username)
61         end
62
63         html("\n")
64         return 0
65 end
66
67
68 -- Returns 1 if the cookie is valid and 0 if it is not.
69 function authenticate_cookie()
70         accepted_users = protected_repos[cgit["repo"]]
71         if accepted_users == nil then
72                 -- We return as valid if the repo is not protected.
73                 return 1
74         end
75
76         local username = validate_value("username", get_cookie(http["cookie"], "cgitauth"))
77         if username == nil or not accepted_users[username:lower()] then
78                 return 0
79         else
80                 return 1
81         end
82 end
83
84 -- Prints the html for the login form.
85 function body()
86         html("<h2>Authentication Required</h2>")
87         html("<form method='post' action='")
88         html_attr(cgit["login"])
89         html("'>")
90         html("<input type='hidden' name='redirect' value='")
91         html_attr(secure_value("redirect", cgit["url"], 0))
92         html("' />")
93         html("<table>")
94         html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>")
95         html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>")
96         html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>")
97         html("</table></form>")
98
99         return 0
100 end
101
102
103
104 --
105 --
106 -- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions.
107 --
108 --
109
110 local actions = {}
111 actions["authenticate-post"] = authenticate_post
112 actions["authenticate-cookie"] = authenticate_cookie
113 actions["body"] = body
114
115 function filter_open(...)
116         action = actions[select(1, ...)]
117
118         http = {}
119         http["cookie"] = select(2, ...)
120         http["method"] = select(3, ...)
121         http["query"] = select(4, ...)
122         http["referer"] = select(5, ...)
123         http["path"] = select(6, ...)
124         http["host"] = select(7, ...)
125         http["https"] = select(8, ...)
126
127         cgit = {}
128         cgit["repo"] = select(9, ...)
129         cgit["page"] = select(10, ...)
130         cgit["url"] = select(11, ...)
131         cgit["login"] = select(12, ...)
132
133 end
134
135 function filter_close()
136         return action()
137 end
138
139 function filter_write(str)
140         post = parse_qs(str)
141 end
142
143
144 --
145 --
146 -- Utility functions based on keplerproject/wsapi.
147 --
148 --
149
150 function url_decode(str)
151         if not str then
152                 return ""
153         end
154         str = string.gsub(str, "+", " ")
155         str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
156         str = string.gsub(str, "\r\n", "\n")
157         return str
158 end
159
160 function url_encode(str)
161         if not str then
162                 return ""
163         end
164         str = string.gsub(str, "\n", "\r\n")
165         str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end)
166         str = string.gsub(str, " ", "+")
167         return str
168 end
169
170 function parse_qs(qs)
171         local tab = {}
172         for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
173                 tab[url_decode(key)] = url_decode(val)
174         end
175         return tab
176 end
177
178 function get_cookie(cookies, name)
179         cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";")
180         return url_decode(string.match(cookies, ";" .. name .. "=(.-);"))
181 end
182
183 function tohex(b)
184         local x = ""
185         for i = 1, #b do
186                 x = x .. string.format("%.2x", string.byte(b, i))
187         end
188         return x
189 end
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." .. tohex(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(tohex(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 chmac = ""
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                         chmac = component
257                 else
258                         break
259                 end
260                 i = i + 1
261         end
262
263         if chmac == nil or chmac:len() == 0 then
264                 return nil
265         end
266
267         -- Lua hashes strings, so these comparisons are time invariant.
268         if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) 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 = tohex(rand.bytes(16))
290         value = url_encode(value)
291         field = url_encode(field)
292         authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
293         authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr))
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