summaryrefslogtreecommitdiff
path: root/auth.cgi
diff options
context:
space:
mode:
Diffstat (limited to 'auth.cgi')
-rwxr-xr-xauth.cgi385
1 files changed, 385 insertions, 0 deletions
diff --git a/auth.cgi b/auth.cgi
new file mode 100755
index 0000000..b6513d6
--- /dev/null
+++ b/auth.cgi
@@ -0,0 +1,385 @@
+#!/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, 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, 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)
+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
+ 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, 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 = 800
+ 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)