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