]> gitweb.ps.run Git - ps-cgit/blob - filters/simple-authentication.lua
4cd498313fd72276f760f1ccb23e518e256f36b5
[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 --
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