Add CSRF token support
This commit is contained in:
@@ -37,6 +37,15 @@ Metrics/MethodLength:
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Enabled: false
|
||||
|
||||
Layout/FirstHashElementIndentation:
|
||||
Enabled: false
|
||||
|
||||
Layout/FirstArrayElementIndentation:
|
||||
Enabled: false
|
||||
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Enabled: false
|
||||
|
||||
|
@@ -8,6 +8,7 @@
|
||||
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="signed_in" content="<%= @signed_in %>">
|
||||
<meta name="csrf" content="<%= @csrf_token %>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
|
@@ -2,10 +2,10 @@
|
||||
module Logman
|
||||
def self.log(log)
|
||||
file, line = caller(1, 1)[0].split(":")
|
||||
File.write("log/main.log", "[#{Time.now}] {#{file}:#{line}} #{log}\n", mode: "a")
|
||||
File.write("log/main.log", "[#{Time.now}] {#{file}:#{line}} #{log}\n", mode: "a") if ENV_HASH["ENV"] == "dev"
|
||||
end
|
||||
|
||||
def self.imp(log)
|
||||
File.write("log/imp.log", "[#{Time.now}] #{log}\n", mode: "a")
|
||||
File.write("log/imp.log", "[#{Time.now}] #{log}\n", mode: "a") if ENV_HASH["ENV"] == "dev"
|
||||
end
|
||||
end
|
||||
|
37
main.rb
37
main.rb
@@ -8,8 +8,6 @@ require "uri"
|
||||
require "xxhash"
|
||||
require "zlib"
|
||||
|
||||
load "logman.rb"
|
||||
|
||||
ALPHANUM = [*"0".."9", *"A".."Z", *"a".."z", "-", "_"].freeze
|
||||
|
||||
env_data = File.exist?(".env") ? File.read(".env") : ""
|
||||
@@ -20,10 +18,9 @@ env_data.each_line do |line|
|
||||
ENV_HASH[match[1]] = match[2]
|
||||
end
|
||||
|
||||
load "logman.rb"
|
||||
# Logman.log ENV_HASH.inspect
|
||||
|
||||
CODE_ENV = :dev
|
||||
|
||||
db_file = File.expand_path("infinsweeper.db")
|
||||
DB = Sequel.connect("sqlite:///#{db_file}", single_threaded: false)
|
||||
DB.run("PRAGMA foreign_keys = ON;")
|
||||
@@ -40,6 +37,8 @@ get "/" do
|
||||
@message = session.message || ""
|
||||
session.message = ""
|
||||
@signed_in = session.signed_in?.nil? ? false : true
|
||||
@csrf_token = Array.new(32) { ALPHANUM.sample }.join
|
||||
session["csrf_token"] = @csrf_token
|
||||
ERB.new(File.read("index.erb")).result(binding)
|
||||
end
|
||||
|
||||
@@ -53,23 +52,28 @@ post "/signup" do
|
||||
uid = session["user"]
|
||||
session.logout unless uid.nil? || $active_users[uid].nil?
|
||||
data = JSON.parse(request.body.read)
|
||||
unless session.csrf_auth?
|
||||
status 401
|
||||
return { "message" => "Unauthorized (invalid CSRF token)!" }.to_json
|
||||
end
|
||||
session["csrf_token"] = Array.new(32) { ALPHANUM.sample }.join
|
||||
if data["email"].nil? || data["pass"].nil? || data["username"].nil?
|
||||
status 400
|
||||
return { "message" => "Bad request made!" }.to_json
|
||||
return { "message" => "Bad request made!", "csrf_token" => session["csrf_token"] }.to_json
|
||||
end
|
||||
signup_status = Players.mk_player(data["username"], data["email"], data["pass"])
|
||||
if signup_status[0] == 200
|
||||
login_status = session.login(data["username"], data["pass"])
|
||||
if login_status[0] == 200
|
||||
status 200
|
||||
return { "message" => login_status[1], "success" => "true" }.to_json
|
||||
return { "message" => login_status[1], "success" => "true", "csrf_token" => session["csrf_token"] }.to_json
|
||||
else
|
||||
status login_status[0]
|
||||
return { "message" => login_status[1] }.to_json
|
||||
return { "message" => login_status[1], "csrf_token" => session["csrf_token"] }.to_json
|
||||
end
|
||||
end
|
||||
status signup_status[0]
|
||||
return { "message" => signup_status[1] }.to_json
|
||||
return { "message" => signup_status[1], "csrf_token" => session["csrf_token"] }.to_json
|
||||
end
|
||||
|
||||
get "/verify/:code" do
|
||||
@@ -82,21 +86,30 @@ post "/login" do
|
||||
data = JSON.parse(request.body.read)
|
||||
session = Sessions.new request, response
|
||||
uid = session["user"]
|
||||
Logman.log(request.env["HTTP_X_CSRF_TOKEN"].to_s + " " + session["csrf_token"].to_s)
|
||||
unless session.csrf_auth?
|
||||
status 401
|
||||
return { "message" => "Unauthorized (invalid CSRF token)!" }.to_json
|
||||
end
|
||||
session["csrf_token"] = Array.new(32) { ALPHANUM.sample }.join
|
||||
if $active_users[uid] && !session.logout
|
||||
status 500
|
||||
return { "message" => "Internal server error when signing the existing session out!" }.to_json
|
||||
return {
|
||||
"message" => "Internal server error when signing the existing session out!",
|
||||
"csrf_token" => session["csrf_token"],
|
||||
}.to_json
|
||||
end
|
||||
if data["username"].nil? || data["pass"].nil?
|
||||
status 400
|
||||
return { "message" => "Bad request made!" }.to_json
|
||||
return { "message" => "Bad request made!", "csrf_token" => session["csrf_token"] }.to_json
|
||||
end
|
||||
login_status = session.login(data["username"], data["pass"])
|
||||
if login_status[0] == 200
|
||||
status 200
|
||||
return { "message" => login_status[1], "success" => "true" }.to_json
|
||||
return { "message" => login_status[1], "success" => "true", "csrf_token" => session["csrf_token"] }.to_json
|
||||
else
|
||||
status login_status[0]
|
||||
return { "message" => login_status[1] }.to_json
|
||||
return { "message" => login_status[1], "csrf_token" => session["csrf_token"] }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
|
@@ -9,21 +9,24 @@ module Players
|
||||
end
|
||||
|
||||
def self.mk_player(username, email, pass)
|
||||
raise ArgumentError, "Email format is wrong!" unless email.match?(/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/)
|
||||
# rubocop:disable Layout/LineLength
|
||||
raise ArgumentError, "Email format is wrong!" unless
|
||||
email.match?(%r[(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])])
|
||||
# rubocop:enable Layout/LineLength
|
||||
raise ArgumentError, "Username must be at least 4 characters long and of valid format." unless
|
||||
username.match?(/\A[a-zA-Z][a-zA-Z0-9_.-]+\z/) && username.length >= 4
|
||||
raise ArgumentError, "Password must be at least 8 characters and of valid format." unless
|
||||
pass.match?(/\A[a-zA-Z0-9_.!?@#$%^&*()+=-]+\z/) && pass.length >= 8
|
||||
|
||||
digest = XXhash.xxh32(pass, ENV_HASH["SALT"])
|
||||
code = CODE_ENV == :prod ? Array.new(24) { ALPHANUM.sample }.join : "!"
|
||||
code = ENV_HASH["ENV"] == "prod" ? Array.new(24) { ALPHANUM.sample }.join : "!"
|
||||
|
||||
DB[
|
||||
"insert into Players (username, digest, email, activation_code) values (?, ?, ?, ?)",
|
||||
username, digest, email, code
|
||||
].insert
|
||||
|
||||
send_email(:new, email, username, code) if CODE_ENV == :prod
|
||||
send_email(:new, email, username, code) if ENV_HASH["ENV"] == "prod"
|
||||
|
||||
[200, "Successfully signed up!"]
|
||||
rescue ArgumentError => e
|
||||
|
@@ -1,3 +1,5 @@
|
||||
const csrfMeta = document.querySelector('meta[name="csrf"]');
|
||||
var csrf = csrfMeta?.content;
|
||||
window.onload = async () => {
|
||||
const popup = document.getElementById("popup");
|
||||
const loginSection = document.getElementById("login");
|
||||
@@ -71,11 +73,12 @@ window.onload = async () => {
|
||||
const { username, pass } = loginForm;
|
||||
const res = await fetch("/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf },
|
||||
body: JSON.stringify({ username: username.value, pass: pass.value }),
|
||||
});
|
||||
const data = await res.json();
|
||||
loginInfo.innerText = data.message;
|
||||
csrf = data.csrf_token;
|
||||
if (data.success == "true") {
|
||||
loginButton.style.display = "none";
|
||||
signupButton.style.display = "none";
|
||||
@@ -91,7 +94,7 @@ window.onload = async () => {
|
||||
const { username, email, pass } = signupForm;
|
||||
const res = await fetch("/signup", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf },
|
||||
body: JSON.stringify({
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
@@ -100,6 +103,7 @@ window.onload = async () => {
|
||||
});
|
||||
const data = await res.json();
|
||||
signupInfo.innerText = data.message;
|
||||
csrf = data.csrf_token;
|
||||
if (data.success == "true") {
|
||||
loginButton.style.display = "none";
|
||||
signupButton.style.display = "none";
|
||||
|
20
session.rb
20
session.rb
@@ -40,17 +40,22 @@ class Sessions
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Use .all here
|
||||
def []=(key, val)
|
||||
session = @request.cookies["session"]
|
||||
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
|
||||
session = JSON.parse(session)
|
||||
session[key] = val
|
||||
Logman.log "Updated: #{key} to #{val}"
|
||||
compressed = Zlib::Deflate.deflate(JSON.generate(session))
|
||||
encoded = Base64.encode64(compressed)
|
||||
@response.set_cookie("session",
|
||||
value: encoded,
|
||||
path: "/",
|
||||
expires: Time.now + 360 * 24 * 60 * 60)
|
||||
expires: Time.now + 360 * 24 * 60 * 60,
|
||||
httponly: true,
|
||||
secure: ENV_HASH["ENV"] == "prod",
|
||||
samesite: :strict)
|
||||
uid = session["user"]
|
||||
DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid
|
||||
rescue JSON::ParserError, Zlib::Error
|
||||
@@ -79,13 +84,19 @@ class Sessions
|
||||
@response.set_cookie("message",
|
||||
value: val,
|
||||
path: "/",
|
||||
expires: Time.now + 360 * 24 * 60 * 60)
|
||||
expires: Time.now + 360 * 24 * 60 * 60,
|
||||
secure: ENV_HASH["ENV"] == "prod",
|
||||
samesite: :strict)
|
||||
end
|
||||
|
||||
def message
|
||||
@request.cookies["message"]
|
||||
end
|
||||
|
||||
def csrf_auth?
|
||||
@request.env["HTTP_X_CSRF_TOKEN"] == self["csrf_token"]
|
||||
end
|
||||
|
||||
def all
|
||||
session = @request.cookies["session"]
|
||||
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
|
||||
@@ -107,7 +118,10 @@ class Sessions
|
||||
@response.set_cookie("session",
|
||||
value: encoded,
|
||||
path: "/",
|
||||
expires: Time.now + 360 * 24 * 60 * 60)
|
||||
expires: Time.now + 360 * 24 * 60 * 60,
|
||||
httponly: true,
|
||||
secure: ENV_HASH["ENV"] == "prod",
|
||||
samesite: :strict)
|
||||
rescue JSON::ParserError, Zlib::Error
|
||||
@response.delete_cookie("session")
|
||||
end
|
||||
|
Reference in New Issue
Block a user