about summary refs log tree commit diff stats
path: root/filters
diff options
context:
space:
mode:
authorJason A. Donenfeld2014-01-14 21:49:31 +0100
committerJason A. Donenfeld2014-01-16 02:28:12 +0100
commitd6e9200cc35411f3f27426b608bcfdef9348e6d3 (patch)
tree9cdd921b03465458d10b99ff4357f79a810501c0 /filters
parentt0111: Additions and fixes (diff)
downloadcgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.tar.gz
cgit-d6e9200cc35411f3f27426b608bcfdef9348e6d3.zip
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.lua225
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
15local protected_repos = {
16 glouglou = { laurent = true, jason = true },
17 qt = { jason = true, bob = true }
18}
19
20local users = {
21 jason = "secretpassword",
22 laurent = "s3cr3t",
23 bob = "ilikelua"
24}
25
26local 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
37function 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
46end
47
48
49-- Returns 1 if the cookie is valid and 0 if it is not.
50function 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
63end
64
65-- Prints the html for the login form.
66function 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
78end
79
80
81--
82--
83-- Cookie construction and validation helpers.
84--
85--
86
87local crypto = require("crypto")
88
89-- Returns username of cookie if cookie is valid. Otherwise returns nil.
90function 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()
133end
134
135function 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")
151end
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
159local actions = {}
160actions["authenticate-post"] = authenticate_post
161actions["authenticate-cookie"] = authenticate_cookie
162actions["body"] = body
163
164function 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
187end
188
189function filter_close()
190 return action()
191end
192
193function filter_write(str)
194 post = parse_qs(str)
195end
196
197
198--
199--
200-- Utility functions follow below, based on keplerproject/wsapi.
201--
202--
203
204function 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
212end
213
214function 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
220end
221
222function get_cookie(cookies, name)
223 cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";")
224 return url_decode(string.match(cookies, ";" .. name .. "=(.-);"))
225end