]> git.cameronkatri.com Git - cgit.git/blob - filters/simple-authentication.lua
auth-filters: generate secret securely
[cgit.git] / 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