1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
|
#!/usr/bin/env lua5.3
local db = require 'db'
local account = require 'account'
local service = require 'service'
local cgi = require 'cgi'
local html = require 'html'
local citrine = require 'citrine'
local forms = require 'forms'
local get, post = {}, {}
local function validate_username(txn, messages, username)
local good = true
if not forms.reasonable_string(username) then
good = false
table.insert(messages, "invalid username")
end
if account.lookup_username(txn, username) then
good = false
table.insert(messages, "this username is already in use.")
end
return good
end
local function validate_email(txn, messages, email)
local good = true
if not forms.reasonable_string(email) then
good = false
table.insert(messages, "invalid email address")
end
if account.uid_of(txn, email) then
good = false
table.insert(messages, "this email is already in use")
end
return good
end
local function register(form)
local messages = {}
if not form.password or not form.email or not form.username then
return false, {"required fields missing"}
end
local txn = db.txn(true)
form.email = form.email:match '^%s*(.*)%s*$'
form.username = form.username:match '^%s*(.*)%s*$'
validate_email(txn, messages, form.email)
validate_username(txn, messages, form.username)
if form.password ~= form.confirm_password then
table.insert(messages, "passwords do not match")
end
if form.coinstar ~= "08:00" then
table.insert(messages,
"please answer the question correctly: when's 8am?")
end
if #messages > 0 then
return false, messages
else
local user = assert(account.create_user(
txn, form.email, form.password, form.username))
local token = user:issue_token "auth.citrons.xyz"
txn:commit()
return true, user.uid, token
end
end
local function login(form)
local txn = db.txn(true)
local user = account.user_from_email(txn, form.email)
if not user then
return false, {"email not found"}
end
if not user:check_password(form.password) then
return false, {"the password entered was incorrect."}
end
local token = user:issue_token "auth.citrons.xyz"
txn:commit()
return true, user.uid, token
end
local function logged_in(uid, token, redirect)
local set_cookie = {
"token="..token.."; Path=/; Secure; HttpOnly; Max-Age=99999999999",
"uid="..uid.."; Path=/; Secure; HttpOnly; Max-Age=99999999999",
}
return {
status = "302 See Other", location = redirect, set_cookie = set_cookie,
content_type = "text/html"
}, html.render_doc(function()
html.a({href = redirect}, "click here to be redirected...")
end)
end
local function get_user(txn, cookie)
if cookie and cookie.uid and cookie.token then
local user = account.get_user(txn, cookie.uid)
if user and user:check_token(cookie.token) == "auth.citrons.xyz" then
return user
end
end
end
local function user_json(user)
local u = {}
u.uid = user.uid
u.username = user:get "username"
u.ttl = 800
return u
end
local function login_page(login_messages, register_messages, values)
return citrine.page {title = "log in", function()
citrine.h1 "log in to auth.citrons.xyz"
html.div({class = 'login-box'}, function()
forms.login(nil, login_messages, values)
forms.register(nil, register_messages, values)
end)
end}
end
local function auth_page(cookie, service_domain, meta)
local sv, err = service.lookup(service_domain)
if not sv then
cgi.abort(503, [[
unable to connect to the requested service. it may be
misconfigured, unavailable, or antimemetic. the error was:
]]..err)
end
local txn = db.txn(true)
local user = get_user(txn, cookie)
local token = user:issue_token(service_domain)
local username = user:get "username"
txn:commit()
local endpoint = sv.endpoint or "login"
if not endpoint:match '^%w+://' then
endpoint = "https://"..service_domain.."/"..endpoint
end
if endpoint:match '^https?://%.*auth%.+citrons%.+xyz%.*' then
cgi.abort(503, "the requested service's endpoint is invalid.")
end
return citrine.page {title = "authenticate", function()
citrine.h1 "authenticate with auth.citrons.xyz"
html.div({class = 'box'}, function()
html.h2(function()
html.text "connect to "
html.i(sv.name or service_domain)
html.text "?"
end)
html.p(function()
html.text "you will be identified as "
html.b(username)
html.text " to "
html.b(service_domain)
html.text "."
end)
forms.connect(user.uid, token, meta, endpoint)
end)
end}
end
get["^/login$"] = function(info)
local txn = db.txn()
local user = get_user(txn, info.cookie)
txn:commit()
if not user then
return 'text/html', login_page()
elseif info.query.service then
return 'text/html', auth_page(
info.cookie, info.query.service, info.query.meta)
else
cgi.redirect(302, "/account")
end
end
post["^/login$"] = function(info)
if not info.form then cgi.abort(400) end
local action = info.form.action
local success, result, token
if action == 'register' then
success, result, token = register(info.form)
elseif action == 'login' then
success, result, token = login(info.form)
else
cgi.abort(400)
end
if not success then
return 'text/html', login_page(
action == 'login' and result,
action == 'register' and result,
info.form)
else
local redirect = "/login"
local service = info.query.service
if service then
redirect = redirect.."?service="..html.url_encode(service)
end
return logged_in(result, token, redirect)
end
end
local function account_page(user, token, messages)
return citrine.page {title = "user profile", function()
citrine.h1 "user profile"
html.div({class = 'box user-settings'}, function()
html.h2(user:get "username")
forms.user_settings(user, token, messages)
end)
end}
end
get["^/account$"] = function(info)
local txn = db.txn()
local user = get_user(txn, info.cookie)
if not user then
cgi.redirect(302, "/login")
end
return 'text/html', account_page(user, info.cookie.token)
end
post["^/account$"] = function(info)
local txn = db.txn(true)
local user = get_user(txn, info.cookie)
if not user then
cgi.redirect(302, "/login")
end
local form = info.form
if not form then
cgi.abort(400)
end
-- prevent CSRF
if form.token ~= info.cookie.token then
cgi.abort(400)
end
if form.logout then
if form.everywhere then
user:revoke_tokens()
else
user:revoke_token(info.cookie.token)
end
txn:commit()
cgi.redirect(302, "/login")
end
local messages = {}
if form.email and form.email ~= user:get "email" then
form.email = form.email:match '^%s*(.*)%s*$'
if validate_email(txn, messages, form.email) then
user:set_email(form.email)
end
end
if form.username and form.username ~= user:get "username" then
form.username = form.username:match '^%s*(.*)%s*$'
if validate_username(txn, messages, form.username) then
user:set_username(form.username)
end
end
if form.password then
if not user:check_password(form.password) then
table.insert(messages, "password supplied is incorrect")
end
if not form.new_password
or form.new_password ~= form.confirm_password then
table.insert(messages, "passwords do not match")
end
if #messages == 0 then
user:set_password(form.new_password)
txn:commit()
return 'text/html', citrine.page{function()
citrine.h1 "password change"
html.p "your password has been changed successfully."
html.p(function()
html.a({href = "/account"}, "return to profile page")
end)
end}
end
end
txn:commit()
user.txn = db.txn()
return 'text/html', account_page(user, info.cookie.token, messages)
end
get["^/api/user/(%w+)$"] = function(info, uid)
local txn = db.txn()
local user = account.get_user(txn, uid)
if not user then cgi.abort(404) end
local j = user_json(user)
return 'application/json', json.encode(j)
end
post["^/api/user/(%w+)/auth/([^/]+)$"] = function(info, uid, service)
local txn = db.txn()
local user = account.get_user(txn, uid)
if not user then cgi.abort(404) end
local j = {}
if not info.form or not info.form.token then
cgi.abort(400)
end
if user:check_token(info.form.token) == service then
j.valid = true
j.user = user_json(user)
j.ttl = 120
else
j.valid = false
j.ttl = 9999999
end
return 'application/json', json.encode(j)
end
post["^/api/user/(%w+)/invalidate$"] = function(info, uid)
local txn = db.txn(true)
local user = account.get_user(txn, uid)
if not user then cgi.abort(404) end
if not info.form or not info.form.token then
cgi.abort(400)
end
if user:check_token(info.form.token) then
user:revoke_token(info.form.token)
end
local j = user_json(user)
txn:commit()
return 'application/json', json.encode(j)
end
get["^/$"] = function()
return 'text/html', citrine.page {function()
citrine.h1 "account services at auth.citrons.xyz"
html.p(function()
html.text [[
experience authentication. auth.citrons.xyz provides a single
account for arbitrary webthings. this service is mostly
intended for webthings made by me
(]]
html.a({href = "https://citrons.xyz"}, "citrons")
html.text ")."
end)
html.p(function()
html.text [[
it is quite easy to integrate auth.citrons.xyz into your own
services. it is however pertinent that you understand that you
do so
]]
html.em "at your own risk"
html.text [[!
I make no guarantees of the security and integrity of this
service. upon comprehending this, you can use the
]]
html.a({href = "/docs"}, "API")
html.text [[
to incorporate the auth.citrons.xyz account system into your
webthing (or non-webthing).
]]
end)
html.img {
class = 'the-graph', src = '/static/graph.svg',
alt = 'graph of world domination'
}
html.h2 "privacy policy"
html.p [[
auth.citrons.xyz retains your supplied account details, such as
your email and username. it also may output your IP address into a
random log file. no data other than this (such as usage patterns,
etc) is collected.
]]
html.p [[
in the event that "hacker" is "able t0 get [my] database
credentials and extract [my] entire database and m0ve the
informati0n t0 an offshore server", they might be able to send you
mean emails. oh no.
]]
end}
end
local function serve(handlers)
return function(info, path)
for patt, handler in pairs(handlers) do
local matches = {path:match(patt)}
if matches[1] then
return handler(info, table.unpack(matches))
end
end
cgi.abort(404)
end
end
cgi.serve({GET = serve(get), POST = serve(post)}, citrine.error)
|