diff options
Diffstat (limited to 'auth.cgi')
-rwxr-xr-x | auth.cgi | 385 |
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) |