diff --git a/.rubocop.yml b/.rubocop.yml
index b15522c..f8c0693 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -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
diff --git a/index.erb b/index.erb
index c808ada..33febfe 100644
--- a/index.erb
+++ b/index.erb
@@ -8,6 +8,7 @@
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
+
"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
diff --git a/players.rb b/players.rb
index cfc0e80..94222e0 100644
--- a/players.rb
+++ b/players.rb
@@ -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
diff --git a/public/src/js/accounts.js b/public/src/js/accounts.js
index e8c6a0f..ef40944 100644
--- a/public/src/js/accounts.js
+++ b/public/src/js/accounts.js
@@ -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";
diff --git a/session.rb b/session.rb
index 34510c9..6489c36 100644
--- a/session.rb
+++ b/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