From c6d948fa01adc0a722265293a9b53ff53333eb5f Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 14 Jul 2021 22:18:01 -0700 Subject: [PATCH 01/22] Add new user field for totp secret --- src/invidious/user/user.cr | 1 + src/invidious/users.cr | 1 + 2 files changed, 2 insertions(+) diff --git a/src/invidious/user/user.cr b/src/invidious/user/user.cr index a6d05fd1..bda72b88 100644 --- a/src/invidious/user/user.cr +++ b/src/invidious/user/user.cr @@ -7,6 +7,7 @@ struct Invidious::User property notifications : Array(String) property subscriptions : Array(String) property email : String + property totp_secret : String? @[DB::Field(converter: Invidious::User::PreferencesConverter)] property preferences : Preferences diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 65566d20..f5488509 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -17,6 +17,7 @@ def create_user(sid, email, password) token: token, watched: [] of String, feed_needs_update: true, + totp_secret: nil, }) return user, sid From 23a71abc11a7aa04dd2810a6dcedf65a7d68ffae Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 14 Jul 2021 23:23:58 -0700 Subject: [PATCH 02/22] Add support for TOTP through Crotp --- locales/en-US.json | 12 +- shard.lock | 21 +++- shard.yml | 2 + src/invidious/helpers/utils.cr | 75 ++++++++++++ src/invidious/routes/account.cr | 135 ++++++++++++++++++++++ src/invidious/routes/login.cr | 6 + src/invidious/routing.cr | 5 + src/invidious/views/user/preferences.ecr | 4 + src/invidious/views/user/setup_2fa.ecr | 36 ++++++ src/invidious/views/user/validate_2fa.ecr | 37 ++++++ 10 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 src/invidious/views/user/setup_2fa.ecr create mode 100644 src/invidious/views/user/validate_2fa.ecr diff --git a/locales/en-US.json b/locales/en-US.json index 74f43d90..664d99a9 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -484,5 +484,15 @@ "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", - "channel_tab_channels_label": "Channels" + "channel_tab_channels_label": "Channels", + "setup-totp-form-header": "Setup two factor authenticiation (TOTP)", + "setup-totp-instructions-download-auth": "Install an authenticator app (or anything that supports totp) on your device", + "setup-totp-instructions-enter-code": "Enter the following secret code:", + "setup-totp-instructions-validate-code": "Enter the 6 digit number on your screen. Be sure to do it under thirty seconds!", + "setup-totp-submit-button": "Setup TOTP", + "general-totp-empty-field": "The TOTP code is a required field", + "general-totp-invalid-code": "The TOTP code entered is invalid", + "general-totp-enter-code-field": "6 digit number", + "general-totp-enter-code-header": "Two-factor authentication", + "general-totp-verify-button": "Verifiy" } diff --git a/shard.lock b/shard.lock index 235e4c25..03f89fbb 100644 --- a/shard.lock +++ b/shard.lock @@ -1,12 +1,24 @@ version: 2.0 shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 0.14.4 + athena-negotiation: git: https://github.com/athena-framework/negotiation.git - version: 0.1.1 + version: 0.1.3 backtracer: git: https://github.com/sija/backtracer.cr.git - version: 1.2.1 + version: 1.2.2 + + base32: + git: https://github.com/philnash/base32.git + version: 0.1.1+git.commit.0a21c1d90731fdefcb3f0db4913f49d3d25350ac + + crotp: + git: https://github.com/philnash/crotp.git + version: 1.0.0 db: git: https://github.com/crystal-lang/crystal-db.git @@ -42,12 +54,9 @@ shards: spectator: git: https://github.com/icy-arctic-fox/spectator.git - version: 0.10.4 + version: 0.10.6 sqlite3: git: https://github.com/crystal-lang/crystal-sqlite3.git version: 0.18.0 - ameba: - git: https://github.com/crystal-ameba/ameba.git - version: 0.14.3 diff --git a/shard.yml b/shard.yml index 7ee0bb2a..301f6c3f 100644 --- a/shard.yml +++ b/shard.yml @@ -31,6 +31,8 @@ dependencies: athena-negotiation: github: athena-framework/negotiation version: ~> 0.1.1 + crotp: + github: philnash/crotp development_dependencies: spectator: diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a006d602..e54aa0a7 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -445,3 +445,78 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) end return text end + +def totp_validator(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + referer = get_referer(env) + + email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254) + password = env.params.body["password"]? + totp_code = env.params.body["totp_code"]? + user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User) + + if !totp_code + return error_template(401, translate(locale, "general-totp-empty-field")) + end + + # Verify if possible + if token = env.params.body["csrf_token"]? + begin + validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) + rescue ex + return error_template(400, ex) + end + end + + totp_instance = CrOTP::TOTP.new(user.totp_secret) + if !totp_instance.verify(totp_code) + return error_template(401, translate(locale, "general-totp-invalid-code")) + end + + if Kemal.config.ssl || CONFIG.https_only + secure = true + else + secure = false + end + + # There are two routes we can go here. + # 1. Where the user is already logged in and is + # confirming an dangerous task. + # 2. The user is logging in. + # + # This can be detected by the hidden email and password parameter + + # https://stackoverflow.com/a/574698 + if email && password + # The rest of the login code. + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) + sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) + PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) + + if CONFIG.domain + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, + secure: secure, http_only: true) + else + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, + secure: secure, http_only: true) + end + else + return error_template(401, "Wrong username or password") + end + + # Since this user has already registered, we don't want to overwrite their preferences + if env.request.cookies["PREFS"]? + cookie = env.request.cookies["PREFS"] + cookie.expires = Time.utc(1990, 1, 1) + env.response.cookies << cookie + end + + env.redirect referer + else + if CONFIG.domain + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: true, expires: Time.utc + 1.hours, secure: secure, http_only: true) + else + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: true, expires: Time.utc + 1.hours, secure: secure, http_only: true) + end + end +end diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 9d930841..f3241057 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -1,5 +1,7 @@ {% skip_file if flag?(:api_only) %} +require "crotp" + module Invidious::Routes::Account extend self @@ -351,4 +353,137 @@ module Invidious::Routes::Account return "{}" end end + + # ------------------- + # 2fa through OTP handling + # ------------------- + def setup_2fa_page(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + user = user.as(User) + sid = sid.as(String) + csrf_token = generate_response(sid, {":setup_2fa"}, HMAC_KEY) + + db_secret = Random::Secure.random_bytes(16).hexstring + totp = CrOTP::TOTP.new(db_secret) + user_secret = totp.base32_secret + + return templated "user/setup_2fa" + end + + # Setup TOTP (post) request. + def setup_2fa(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env) + + if !user + return env.redirect referer + end + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, locale) + rescue ex + return error_template(400, ex) + end + + totp_code = env.params.body["totp_code"]? + db_secret = env.params.body["db_secret"] # Must exist + if !totp_code + return error_template(401, translate(locale, "general-totp-empty-field")) + end + + totp_instance = CrOTP::TOTP.new(db_secret) + if !totp_instance.verify(totp_code) + return error_template(401, translate(locale, "general-totp-invalid-code")) + end + + PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", db_secret.to_s, user.email) + end + + # Validate 2fa code endpoint + def validate_2fa(env) + locale = env.get("preferences").as(Preferences).locale + referer = get_referer(env) + + email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254) + password = env.params.body["password"]? + totp_code = env.params.body["totp_code"]? + # This endpoint is only called when the user has a totp_secret. + user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User).not_nil! + + if !totp_code + return error_template(401, translate(locale, "general-totp-empty-field")) + end + + totp_instance = CrOTP::TOTP.new(user.totp_secret.not_nil!) + if !totp_instance.verify(totp_code) + return error_template(401, translate(locale, "general-totp-invalid-code")) + end + + if Kemal.config.ssl || CONFIG.https_only + secure = true + else + secure = false + end + + # There are two routes we can go here. + # 1. Where the user is already logged in and is + # confirming an dangerous task. + # 2. The user is logging in. + # + # This can be detected by the hidden email and password parameter + + # https://stackoverflow.com/a/574698 + if email && password + # The rest of the login code. + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) + sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) + PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) + + if CONFIG.domain + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, + secure: secure, http_only: true) + else + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, + secure: secure, http_only: true) + end + else + return error_template(401, "Wrong username or password") + end + + # Since this user has already registered, we don't want to overwrite their preferences + if env.request.cookies["PREFS"]? + cookie = env.request.cookies["PREFS"] + cookie.expires = Time.utc(1990, 1, 1) + env.response.cookies << cookie + end + + env.redirect referer + else + token = env.params.body["csrf_token"] + + begin + validate_request(token, env.get?("sid").as(String), env.request, HMAC_KEY, locale) + rescue ex + return error_template(400, ex) + end + + if CONFIG.domain + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true) + else + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true) + end + end + end end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index d0f7ac22..44aea163 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -56,6 +56,12 @@ module Invidious::Routes::Login user = Invidious::Database::Users.select(email: email) if user + # If user has setup TOTP + if user.totp_secret + csrf_token = nil # setting this to false for compatibility reasons. + return templated "user/validate_2fa" + end + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) Invidious::Database::SessionIDs.insert(sid, email) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9c43171c..fc21f976 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -78,6 +78,11 @@ module Invidious::Routing post "/token_ajax", Routes::Account, :token_ajax post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription get "/subscription_manager", Routes::Subscriptions, :subscription_manager + + # 2fa routes + Invidious::Routing.get "/setup_2fa", Routes::Account, :setup_2fa_page + Invidious::Routing.post "/setup_2fa", Routes::Account, :setup_2fa + Invidious::Routing.post "/validate_2fa", Routes::Account, :validate_2fa end def register_iv_playlist_routes diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index dfda1434..a0f77dbf 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -345,6 +345,10 @@ <%= translate(locale, "Watch history") %> + + diff --git a/src/invidious/views/user/setup_2fa.ecr b/src/invidious/views/user/setup_2fa.ecr new file mode 100644 index 00000000..a22e5f0b --- /dev/null +++ b/src/invidious/views/user/setup_2fa.ecr @@ -0,0 +1,36 @@ +<% content_for "header" do %> +<%= translate(locale, "setup-totp-form-header") %> - Invidious +<% end %> + +
+
+
+
+
+ <%= translate(locale, "setup-totp-form-header") %> +
+ + + +
    +
  1. <%= translate(locale, "setup-totp-instructions-download-auth") %>
  2. +
  3. <%= translate(locale, "setup-totp-instructions-enter-code") %> + <%=user_secret%> +
  4. +
  5. <%= translate(locale, "setup-totp-instructions-validate-code") %>
  6. +
+ + "> + + + + + +
+
+
+
+
+
diff --git a/src/invidious/views/user/validate_2fa.ecr b/src/invidious/views/user/validate_2fa.ecr new file mode 100644 index 00000000..3e477689 --- /dev/null +++ b/src/invidious/views/user/validate_2fa.ecr @@ -0,0 +1,37 @@ +<% content_for "header" do %> +<%= translate(locale, "setup-totp-form-header") %> - Invidious +<% end %> + +
+
+
+
+
+ <%= translate(locale, "general-totp-enter-code-header") %> +
+ + + <% if email %> + + <% end %> + <% if password %> + + <% end %> + + "> + + <% if csrf_token %> + + <% end %> + + + + +
+
+
+
+
+
From 68a216102e81531a170e9e86310adc6c46435b24 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 01:19:55 -0700 Subject: [PATCH 03/22] Add 2fa to change_password endpoint --- src/invidious/routes/account.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index f3241057..1b55db9e 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -23,6 +23,12 @@ module Invidious::Routes::Account user = user.as(User) sid = sid.as(String) + + if user.totp_secret && env.response.cookies["2faVerified"]?.try &.value != "1" || nil + csrf_token = generate_response(sid, {":validate_2fa"}, HMAC_KEY) + next templated "account/validate_2fa?referer=#{env.get?("current_page")}" + end + csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY) templated "user/change_password" @@ -362,7 +368,7 @@ module Invidious::Routes::Account user = env.get? "user" sid = env.get? "sid" - referer = get_referer(env) + referer = get_referer(env, unroll: false) user = user.as(User) sid = sid.as(String) From adbbd609e5d58742ed1ebff452e9a66dca7ef36f Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 01:27:27 -0700 Subject: [PATCH 04/22] Fixes + add 2fa to pass change and acc delete --- src/invidious/helpers/utils.cr | 7 +++++++ src/invidious/routes/account.cr | 12 +++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index e54aa0a7..9e6aa2c7 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -520,3 +520,10 @@ def totp_validator(env) end end end + +def call_totp_validator(env, user, sid, locale) + referer = URI.decode_www_form(env.get?("current_page").to_s) + csrf_token = generate_response(sid, {":validate_2fa"}, HMAC_KEY) + email, password = {user.email, nil} + return templated "user/validate_2fa" +end diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 1b55db9e..e69f118a 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -25,8 +25,7 @@ module Invidious::Routes::Account sid = sid.as(String) if user.totp_secret && env.response.cookies["2faVerified"]?.try &.value != "1" || nil - csrf_token = generate_response(sid, {":validate_2fa"}, HMAC_KEY) - next templated "account/validate_2fa?referer=#{env.get?("current_page")}" + return call_totp_validator(env, user, sid, locale) end csrf_token = generate_response(sid, {":change_password"}, HMAC_KEY) @@ -104,6 +103,11 @@ module Invidious::Routes::Account user = user.as(User) sid = sid.as(String) + + if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil + return call_totp_validator(env, user, sid, locale) + end + csrf_token = generate_response(sid, {":delete_account"}, HMAC_KEY) templated "user/delete_account" @@ -420,7 +424,7 @@ module Invidious::Routes::Account # Validate 2fa code endpoint def validate_2fa(env) locale = env.get("preferences").as(Preferences).locale - referer = get_referer(env) + referer = get_referer(env, unroll: false) email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254) password = env.params.body["password"]? @@ -491,5 +495,7 @@ module Invidious::Routes::Account env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true) end end + + env.redirect referer end end From 9a615ac1af7c29332650386620f78a55d340d980 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 02:32:53 -0700 Subject: [PATCH 05/22] Add 2fa to token auth endpoint --- src/invidious/routes/account.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index e69f118a..23101eed 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -207,6 +207,11 @@ module Invidious::Routes::Account user = env.get? "user" sid = env.get? "sid" + + if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil + return call_totp_validator(env, user, sid, locale) + end + referer = get_referer(env) if !user From f32f0b6e16f4f0fa00385a4ad0a3f24365030273 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 02:42:44 -0700 Subject: [PATCH 06/22] Add endpoint to disable 2fa --- locales/en-US.json | 4 ++- src/invidious/helpers/utils.cr | 2 +- src/invidious/routes/account.cr | 42 +++++++++++++++++++++++-- src/invidious/routing.cr | 2 ++ src/invidious/views/user/remove_2fa.ecr | 24 ++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/invidious/views/user/remove_2fa.ecr diff --git a/locales/en-US.json b/locales/en-US.json index 664d99a9..b3eb8d18 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -494,5 +494,7 @@ "general-totp-invalid-code": "The TOTP code entered is invalid", "general-totp-enter-code-field": "6 digit number", "general-totp-enter-code-header": "Two-factor authentication", - "general-totp-verify-button": "Verifiy" + "general-totp-verify-button": "Verify", + "remove-totp-header": "Remove two-factor authentication", + "remove-totp-confirm-message": "Are you sure you would like to remove two-factor-authentication?" } diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 9e6aa2c7..edbb3b94 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -462,7 +462,7 @@ def totp_validator(env) # Verify if possible if token = env.params.body["csrf_token"]? begin - validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) + validate_request(token, sid, env.request, HMAC_KEY, locale) rescue ex return error_template(400, ex) end diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 23101eed..d5b6ce42 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -208,6 +208,9 @@ module Invidious::Routes::Account user = env.get? "user" sid = env.get? "sid" + user = user.as(User) + sid = sid.as(String) + if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil return call_totp_validator(env, user, sid, locale) end @@ -218,8 +221,6 @@ module Invidious::Routes::Account return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}" end - user = user.as(User) - sid = sid.as(String) csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY) scopes = env.params.query["scopes"]?.try &.split(",") @@ -503,4 +504,41 @@ module Invidious::Routes::Account env.redirect referer end + + # Endpoint to remove 2fa + def remove_2fa_page(env) + locale = env.get("preferences").as(Preferences).locale + referer = get_referer(env) + + user = env.get("user").as(User) + sid = env.get("sid").as(String) + csrf_token = generate_response(sid, {":remove_2fa"}, HMAC_KEY) + + return templated "user/remove_2fa" + end + + # Remove 2fa post request. + def remove_2fa(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env, unroll: false) + + if !user + return env.redirect referer + end + + user = user.as(User) + sid = sid.as(String) + token = env.params.body["csrf_token"]? + + begin + validate_request(token, sid, env.request, HMAC_KEY, locale) + rescue ex + return error_template(400, ex) + end + + PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", nil, user.email) + end end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index fc21f976..27c08f3d 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -83,6 +83,8 @@ module Invidious::Routing Invidious::Routing.get "/setup_2fa", Routes::Account, :setup_2fa_page Invidious::Routing.post "/setup_2fa", Routes::Account, :setup_2fa Invidious::Routing.post "/validate_2fa", Routes::Account, :validate_2fa + Invidious::Routing.get "/remove_2fa", Routes::Account, :remove_2fa_page + Invidious::Routing.post "/remove_2fa", Routes::Account, :remove_2fa end def register_iv_playlist_routes diff --git a/src/invidious/views/user/remove_2fa.ecr b/src/invidious/views/user/remove_2fa.ecr new file mode 100644 index 00000000..47f92263 --- /dev/null +++ b/src/invidious/views/user/remove_2fa.ecr @@ -0,0 +1,24 @@ +<% content_for "header" do %> +<%= translate(locale, "remove-totp-header") %> - Invidious +<% end %> + +
+
+ <%= translate(locale, "remove-totp-confirm-message") %> + +
+
+ +
+ +
+ + +
+
From 2bf8bfd5de1d19dfdbd603eefe754a6f2dc843be Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 02:45:23 -0700 Subject: [PATCH 07/22] Fix referrer for setup_2fa page --- src/invidious/routes/account.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index d5b6ce42..b5cbd188 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -397,7 +397,7 @@ module Invidious::Routes::Account user = env.get? "user" sid = env.get? "sid" - referer = get_referer(env) + referer = get_referer(env, unroll: false) if !user return env.redirect referer @@ -425,6 +425,7 @@ module Invidious::Routes::Account end PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", db_secret.to_s, user.email) + env.redirect referer end # Validate 2fa code endpoint From 379cad6bcd1d0b267c457d0ca64ea17612535e9e Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 02:53:35 -0700 Subject: [PATCH 08/22] Change 2fa on login to be after pass verification --- src/invidious/routes/account.cr | 8 +++++--- src/invidious/routes/login.cr | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index b5cbd188..0f751694 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -373,6 +373,8 @@ module Invidious::Routes::Account # ------------------- # 2fa through OTP handling # ------------------- + + # Setup 2fa page def setup_2fa_page(env) locale = env.get("preferences").as(Preferences).locale @@ -391,7 +393,7 @@ module Invidious::Routes::Account return templated "user/setup_2fa" end - # Setup TOTP (post) request. + # Setup 2fa post request. def setup_2fa(env) locale = env.get("preferences").as(Preferences).locale @@ -463,7 +465,7 @@ module Invidious::Routes::Account # https://stackoverflow.com/a/574698 if email && password - # The rest of the login code. + # Verify the password again for extra security if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) @@ -506,7 +508,7 @@ module Invidious::Routes::Account env.redirect referer end - # Endpoint to remove 2fa + # Remove 2fa page def remove_2fa_page(env) locale = env.get("preferences").as(Preferences).locale referer = get_referer(env) diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 44aea163..ed2185b8 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -56,13 +56,13 @@ module Invidious::Routes::Login user = Invidious::Database::Users.select(email: email) if user - # If user has setup TOTP - if user.totp_secret - csrf_token = nil # setting this to false for compatibility reasons. - return templated "user/validate_2fa" - end + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) \ + # If the password is correct then we'll go ahead and begin 2fa if applicable + if user.totp_secret + csrf_token = nil # setting this to false for compatibility reasons. + return templated "user/validate_2fa" + end - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) Invidious::Database::SessionIDs.insert(sid, email) From f08f8b67be7eae2317f8742009cabaca5b726c2a Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 03:00:47 -0700 Subject: [PATCH 09/22] Add migration script for totp --- config/migrate-scripts/migrate-db-63162986.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 config/migrate-scripts/migrate-db-63162986.sh diff --git a/config/migrate-scripts/migrate-db-63162986.sh b/config/migrate-scripts/migrate-db-63162986.sh new file mode 100644 index 00000000..757d5994 --- /dev/null +++ b/config/migrate-scripts/migrate-db-63162986.sh @@ -0,0 +1 @@ +psql invidious kemal -c "ALTER TABLE users ADD COLUMN totp_secret VARCHAR(128)" \ No newline at end of file From 77cde691c99c357753be759854810005c913c411 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 15 Jul 2021 03:03:39 -0700 Subject: [PATCH 10/22] Remove totp_validator method used for testing... Oops --- src/invidious/helpers/utils.cr | 74 ---------------------------------- 1 file changed, 74 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index edbb3b94..17d8098f 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -446,80 +446,6 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) return text end -def totp_validator(env) - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - referer = get_referer(env) - - email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254) - password = env.params.body["password"]? - totp_code = env.params.body["totp_code"]? - user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User) - - if !totp_code - return error_template(401, translate(locale, "general-totp-empty-field")) - end - - # Verify if possible - if token = env.params.body["csrf_token"]? - begin - validate_request(token, sid, env.request, HMAC_KEY, locale) - rescue ex - return error_template(400, ex) - end - end - - totp_instance = CrOTP::TOTP.new(user.totp_secret) - if !totp_instance.verify(totp_code) - return error_template(401, translate(locale, "general-totp-invalid-code")) - end - - if Kemal.config.ssl || CONFIG.https_only - secure = true - else - secure = false - end - - # There are two routes we can go here. - # 1. Where the user is already logged in and is - # confirming an dangerous task. - # 2. The user is logging in. - # - # This can be detected by the hidden email and password parameter - - # https://stackoverflow.com/a/574698 - if email && password - # The rest of the login code. - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) - sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) - PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) - - if CONFIG.domain - env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, - secure: secure, http_only: true) - else - env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, - secure: secure, http_only: true) - end - else - return error_template(401, "Wrong username or password") - end - - # Since this user has already registered, we don't want to overwrite their preferences - if env.request.cookies["PREFS"]? - cookie = env.request.cookies["PREFS"] - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie - end - - env.redirect referer - else - if CONFIG.domain - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: true, expires: Time.utc + 1.hours, secure: secure, http_only: true) - else - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: true, expires: Time.utc + 1.hours, secure: secure, http_only: true) - end - end -end def call_totp_validator(env, user, sid, locale) referer = URI.decode_www_form(env.get?("current_page").to_s) From 482825309317f382cff656b6378353039da0408a Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 13:32:56 -0700 Subject: [PATCH 11/22] Update 2fa locales to use _ instead of - --- locales/en-US.json | 25 +++++++++++------------ src/invidious/views/user/preferences.ecr | 2 +- src/invidious/views/user/remove_2fa.ecr | 4 ++-- src/invidious/views/user/setup_2fa.ecr | 16 +++++++-------- src/invidious/views/user/validate_2fa.ecr | 8 ++++---- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index b3eb8d18..4f57abff 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -485,16 +485,15 @@ "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels", - "setup-totp-form-header": "Setup two factor authenticiation (TOTP)", - "setup-totp-instructions-download-auth": "Install an authenticator app (or anything that supports totp) on your device", - "setup-totp-instructions-enter-code": "Enter the following secret code:", - "setup-totp-instructions-validate-code": "Enter the 6 digit number on your screen. Be sure to do it under thirty seconds!", - "setup-totp-submit-button": "Setup TOTP", - "general-totp-empty-field": "The TOTP code is a required field", - "general-totp-invalid-code": "The TOTP code entered is invalid", - "general-totp-enter-code-field": "6 digit number", - "general-totp-enter-code-header": "Two-factor authentication", - "general-totp-verify-button": "Verify", - "remove-totp-header": "Remove two-factor authentication", - "remove-totp-confirm-message": "Are you sure you would like to remove two-factor-authentication?" -} + "setup_totp_form_header": "Setup two factor authenticiation (TOTP)", + "setup_totp_instructions_download_auth": "Install an authenticator app (or anything that supports totp) on your device", + "setup_totp_instructions_enter_code": "Enter the following secret code:", + "setup_totp_instructions_validate_code": "Enter the 6 digit number on your screen. Be sure to do it under thirty seconds!", + "setup_totp_submit_button": "Setup TOTP", + "general_totp_empty_field": "The TOTP code is a required field", + "general_totp_invalid_code": "The TOTP code entered is invalid", + "general_totp_enter_code_field": "6 digit number", + "general_totp_enter_code_header": "Two-factor authentication", + "general_totp_verify_button": "Verify", + "remove_totp_header": "Remove two-factor authentication", + "remove_totp_confirm_message": "Are you sure you would like to remove two-factor-authentication?"} diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index a0f77dbf..3ad38f0e 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -346,7 +346,7 @@
diff --git a/src/invidious/views/user/remove_2fa.ecr b/src/invidious/views/user/remove_2fa.ecr index 47f92263..540b99d2 100644 --- a/src/invidious/views/user/remove_2fa.ecr +++ b/src/invidious/views/user/remove_2fa.ecr @@ -1,10 +1,10 @@ <% content_for "header" do %> -<%= translate(locale, "remove-totp-header") %> - Invidious +<%= translate(locale, "remove_totp_header") %> - Invidious <% end %>
- <%= translate(locale, "remove-totp-confirm-message") %> + <%= translate(locale, "remove_totp_confirm_message") %>
diff --git a/src/invidious/views/user/setup_2fa.ecr b/src/invidious/views/user/setup_2fa.ecr index a22e5f0b..deafe7ca 100644 --- a/src/invidious/views/user/setup_2fa.ecr +++ b/src/invidious/views/user/setup_2fa.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> -<%= translate(locale, "setup-totp-form-header") %> - Invidious +<%= translate(locale, "setup_totp_form_header") %> - Invidious <% end %>
@@ -7,25 +7,25 @@
- <%= translate(locale, "setup-totp-form-header") %> + <%= translate(locale, "setup_totp_form_header") %>
    -
  1. <%= translate(locale, "setup-totp-instructions-download-auth") %>
  2. -
  3. <%= translate(locale, "setup-totp-instructions-enter-code") %> +
  4. <%= translate(locale, "setup_totp_instructions_download_auth") %>
  5. +
  6. <%= translate(locale, "setup_totp_instructions_enter_code") %> <%=user_secret%>
  7. -
  8. <%= translate(locale, "setup-totp-instructions-validate-code") %>
  9. +
  10. <%= translate(locale, "setup_totp_instructions_validate_code") %>
- "> + "> -
diff --git a/src/invidious/views/user/validate_2fa.ecr b/src/invidious/views/user/validate_2fa.ecr index 3e477689..9d2b5d0b 100644 --- a/src/invidious/views/user/validate_2fa.ecr +++ b/src/invidious/views/user/validate_2fa.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> -<%= translate(locale, "setup-totp-form-header") %> - Invidious +<%= translate(locale, "setup_totp_form_header") %> - Invidious <% end %>
@@ -7,7 +7,7 @@
- <%= translate(locale, "general-totp-enter-code-header") %> + <%= translate(locale, "general_totp_enter_code_header") %>
@@ -18,7 +18,7 @@ <% end %> - "> + "> <% if csrf_token %> @@ -26,7 +26,7 @@
From 2928e3e80e6bffb6317af9db28ea4e902e7bf1e5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 13:34:53 -0700 Subject: [PATCH 12/22] Update 2fa routes to be in the form of /2fa/ --- src/invidious/helpers/utils.cr | 2 +- src/invidious/routes/account.cr | 12 ++++++------ src/invidious/routes/login.cr | 4 ++-- src/invidious/routing.cr | 10 +++++----- src/invidious/views/user/preferences.ecr | 2 +- src/invidious/views/user/remove_2fa.ecr | 2 +- src/invidious/views/user/setup_2fa.ecr | 2 +- src/invidious/views/user/validate_2fa.ecr | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 17d8098f..53ae35f1 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -449,7 +449,7 @@ end def call_totp_validator(env, user, sid, locale) referer = URI.decode_www_form(env.get?("current_page").to_s) - csrf_token = generate_response(sid, {":validate_2fa"}, HMAC_KEY) + csrf_token = generate_response(sid, {":2fa/validate"}, HMAC_KEY) email, password = {user.email, nil} return templated "user/validate_2fa" end diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 0f751694..1abf240f 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -384,7 +384,7 @@ module Invidious::Routes::Account user = user.as(User) sid = sid.as(String) - csrf_token = generate_response(sid, {":setup_2fa"}, HMAC_KEY) + csrf_token = generate_response(sid, {":2fa/setup"}, HMAC_KEY) db_secret = Random::Secure.random_bytes(16).hexstring totp = CrOTP::TOTP.new(db_secret) @@ -472,10 +472,10 @@ module Invidious::Routes::Account if CONFIG.domain env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, - secure: secure, http_only: true) + secure: secure, http_only: true, path: "/") else env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, - secure: secure, http_only: true) + secure: secure, http_only: true, path: "/") end else return error_template(401, "Wrong username or password") @@ -499,9 +499,9 @@ module Invidious::Routes::Account end if CONFIG.domain - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true) + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true, path: "/") else - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true) + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true, path: "/") end end @@ -515,7 +515,7 @@ module Invidious::Routes::Account user = env.get("user").as(User) sid = env.get("sid").as(String) - csrf_token = generate_response(sid, {":remove_2fa"}, HMAC_KEY) + csrf_token = generate_response(sid, {":2fa/remove"}, HMAC_KEY) return templated "user/remove_2fa" end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index ed2185b8..f50823a0 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -56,8 +56,8 @@ module Invidious::Routes::Login user = Invidious::Database::Users.select(email: email) if user - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) \ - # If the password is correct then we'll go ahead and begin 2fa if applicable + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) + # If the password is correct then we'll go ahead and begin 2fa if applicable if user.totp_secret csrf_token = nil # setting this to false for compatibility reasons. return templated "user/validate_2fa" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 27c08f3d..01d1d79a 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -80,11 +80,11 @@ module Invidious::Routing get "/subscription_manager", Routes::Subscriptions, :subscription_manager # 2fa routes - Invidious::Routing.get "/setup_2fa", Routes::Account, :setup_2fa_page - Invidious::Routing.post "/setup_2fa", Routes::Account, :setup_2fa - Invidious::Routing.post "/validate_2fa", Routes::Account, :validate_2fa - Invidious::Routing.get "/remove_2fa", Routes::Account, :remove_2fa_page - Invidious::Routing.post "/remove_2fa", Routes::Account, :remove_2fa + Invidious::Routing.get "/2fa/setup", Routes::Account, :setup_2fa_page + Invidious::Routing.post "/2fa/setup", Routes::Account, :setup_2fa + Invidious::Routing.get "/2fa/remove", Routes::Account, :remove_2fa_page + Invidious::Routing.post "/2fa/remove", Routes::Account, :remove_2fa + Invidious::Routing.post "/2fa/validate", Routes::Account, :validate_2fa end def register_iv_playlist_routes diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 3ad38f0e..d6a220c4 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -346,7 +346,7 @@
diff --git a/src/invidious/views/user/remove_2fa.ecr b/src/invidious/views/user/remove_2fa.ecr index 540b99d2..000a197c 100644 --- a/src/invidious/views/user/remove_2fa.ecr +++ b/src/invidious/views/user/remove_2fa.ecr @@ -3,7 +3,7 @@ <% end %>
- + <%= translate(locale, "remove_totp_confirm_message") %>
diff --git a/src/invidious/views/user/setup_2fa.ecr b/src/invidious/views/user/setup_2fa.ecr index deafe7ca..2594ea72 100644 --- a/src/invidious/views/user/setup_2fa.ecr +++ b/src/invidious/views/user/setup_2fa.ecr @@ -6,7 +6,7 @@
- + <%= translate(locale, "setup_totp_form_header") %>
diff --git a/src/invidious/views/user/validate_2fa.ecr b/src/invidious/views/user/validate_2fa.ecr index 9d2b5d0b..5732e104 100644 --- a/src/invidious/views/user/validate_2fa.ecr +++ b/src/invidious/views/user/validate_2fa.ecr @@ -6,7 +6,7 @@
- + <%= translate(locale, "general_totp_enter_code_header") %>
From 194d248342b22ec2e6f641827657bbb287c7c0a2 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 14:25:50 -0700 Subject: [PATCH 13/22] Document 2fa functions --- src/invidious/helpers/utils.cr | 4 ++++ src/invidious/routes/account.cr | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 53ae35f1..b9bb3121 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -447,6 +447,10 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) end +# Templates the 2fa validator page. +# +# Requires the env, user, sid and locale variables for +# generating a csrf_token and the required variables for the view. def call_totp_validator(env, user, sid, locale) referer = URI.decode_www_form(env.get?("current_page").to_s) csrf_token = generate_response(sid, {":2fa/validate"}, HMAC_KEY) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 1abf240f..0cead392 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -374,7 +374,7 @@ module Invidious::Routes::Account # 2fa through OTP handling # ------------------- - # Setup 2fa page + # Templates the page to setup 2fa on an user account def setup_2fa_page(env) locale = env.get("preferences").as(Preferences).locale @@ -382,6 +382,10 @@ module Invidious::Routes::Account sid = env.get? "sid" referer = get_referer(env, unroll: false) + if !user + return env.redirect referer + end + user = user.as(User) sid = sid.as(String) csrf_token = generate_response(sid, {":2fa/setup"}, HMAC_KEY) @@ -393,7 +397,7 @@ module Invidious::Routes::Account return templated "user/setup_2fa" end - # Setup 2fa post request. + # Handles requests to setup 2fa on an user account def setup_2fa(env) locale = env.get("preferences").as(Preferences).locale @@ -430,7 +434,7 @@ module Invidious::Routes::Account env.redirect referer end - # Validate 2fa code endpoint + # Handles requests to validate a TOTP code on an user account def validate_2fa(env) locale = env.get("preferences").as(Preferences).locale referer = get_referer(env, unroll: false) @@ -508,7 +512,7 @@ module Invidious::Routes::Account env.redirect referer end - # Remove 2fa page + # Templates the page to remove 2fa on an user account def remove_2fa_page(env) locale = env.get("preferences").as(Preferences).locale referer = get_referer(env) From d51d53e04b479ed34b47cda81ba1d16455248407 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 14:28:26 -0700 Subject: [PATCH 14/22] Add endpoint to remove TOTP to UI and fix typos --- locales/en-US.json | 6 +++--- src/invidious/views/user/preferences.ecr | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index 4f57abff..8dae632f 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -485,8 +485,8 @@ "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels", - "setup_totp_form_header": "Setup two factor authenticiation (TOTP)", - "setup_totp_instructions_download_auth": "Install an authenticator app (or anything that supports totp) on your device", + "setup_totp_form_header": "Setup two-factor authentication (TOTP)", + "setup_totp_instructions_download_auth": "Install an authenticator app (or anything that supports TOTP) on your device", "setup_totp_instructions_enter_code": "Enter the following secret code:", "setup_totp_instructions_validate_code": "Enter the 6 digit number on your screen. Be sure to do it under thirty seconds!", "setup_totp_submit_button": "Setup TOTP", @@ -495,5 +495,5 @@ "general_totp_enter_code_field": "6 digit number", "general_totp_enter_code_header": "Two-factor authentication", "general_totp_verify_button": "Verify", - "remove_totp_header": "Remove two-factor authentication", + "remove_totp_header": "Remove two-factor authentication (TOTP)", "remove_totp_confirm_message": "Are you sure you would like to remove two-factor-authentication?"} diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index d6a220c4..8c6cc7f3 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -346,7 +346,11 @@
From e93c8672b4e4f9fc494ca647347ae7411136cff8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 14:34:31 -0700 Subject: [PATCH 15/22] Redirect to referer after 2fa removal --- src/invidious/routes/account.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 0cead392..2f3194f3 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -524,7 +524,7 @@ module Invidious::Routes::Account return templated "user/remove_2fa" end - # Remove 2fa post request. + # Handles requests to remove 2fa on an user account def remove_2fa(env) locale = env.get("preferences").as(Preferences).locale @@ -547,5 +547,6 @@ module Invidious::Routes::Account end PG_DB.exec("UPDATE users SET totp_secret = $1 WHERE email = $2", nil, user.email) + env.redirect referer end end From ba067e3deb7677e8f29054e747a7fd66cfe6db77 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 14:37:08 -0700 Subject: [PATCH 16/22] Only allow totp removal endpoint for users w/ 2fa --- src/invidious/routes/account.cr | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 2f3194f3..fb37c993 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -515,10 +515,17 @@ module Invidious::Routes::Account # Templates the page to remove 2fa on an user account def remove_2fa_page(env) locale = env.get("preferences").as(Preferences).locale - referer = get_referer(env) - user = env.get("user").as(User) - sid = env.get("sid").as(String) + user = env.get? "user" + sid = env.get? "sid" + referer = get_referer(env, unroll: false) + + if !user || user.is_a? User && !user.totp_secret + return env.redirect referer + end + + user = user.as(User) + sid = sid.as(String) csrf_token = generate_response(sid, {":2fa/remove"}, HMAC_KEY) return templated "user/remove_2fa" @@ -532,7 +539,7 @@ module Invidious::Routes::Account sid = env.get? "sid" referer = get_referer(env, unroll: false) - if !user + if !user || user.is_a? User && !user.totp_secret return env.redirect referer end From 71f3053c7bb6ef55c2f6e95f83c4844e79a0a123 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 16 Jul 2021 14:47:11 -0700 Subject: [PATCH 17/22] add new totp_secret column to sql config --- config/sql/users.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/config/sql/users.sql b/config/sql/users.sql index ad002ec2..2da3becf 100644 --- a/config/sql/users.sql +++ b/config/sql/users.sql @@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS public.users token text, watched text[], feed_needs_update boolean, + totp_secret VARCHAR(128) CONSTRAINT users_email_key UNIQUE (email) ); From eb70eb374741552ca137f5d148c62b366da3799d Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 25 Jul 2023 15:40:06 -0700 Subject: [PATCH 18/22] Rebase fixes --- config/migrate-scripts/migrate-db-63162986.sh | 1 - .../migrations/0011_add_totp_secret_to_users_table.cr | 11 +++++++++++ src/invidious/routes/account.cr | 11 +++++------ 3 files changed, 16 insertions(+), 7 deletions(-) delete mode 100644 config/migrate-scripts/migrate-db-63162986.sh create mode 100644 src/invidious/database/migrations/0011_add_totp_secret_to_users_table.cr diff --git a/config/migrate-scripts/migrate-db-63162986.sh b/config/migrate-scripts/migrate-db-63162986.sh deleted file mode 100644 index 757d5994..00000000 --- a/config/migrate-scripts/migrate-db-63162986.sh +++ /dev/null @@ -1 +0,0 @@ -psql invidious kemal -c "ALTER TABLE users ADD COLUMN totp_secret VARCHAR(128)" \ No newline at end of file diff --git a/src/invidious/database/migrations/0011_add_totp_secret_to_users_table.cr b/src/invidious/database/migrations/0011_add_totp_secret_to_users_table.cr new file mode 100644 index 00000000..2501123d --- /dev/null +++ b/src/invidious/database/migrations/0011_add_totp_secret_to_users_table.cr @@ -0,0 +1,11 @@ +module Invidious::Database::Migrations + class AddTotpSecretToUsersTable < Migration + version 11 + + def up(conn : DB::Connection) + conn.exec <<-SQL + ALTER TABLE users ADD COLUMN totp_secret VARCHAR(128) + SQL + end + end +end diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index fb37c993..c61767fb 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -24,7 +24,7 @@ module Invidious::Routes::Account user = user.as(User) sid = sid.as(String) - if user.totp_secret && env.response.cookies["2faVerified"]?.try &.value != "1" || nil + if user.totp_secret && env.request.cookies["2faVerified"]?.try &.value != "1" || nil return call_totp_validator(env, user, sid, locale) end @@ -461,15 +461,14 @@ module Invidious::Routes::Account end # There are two routes we can go here. - # 1. Where the user is already logged in and is - # confirming an dangerous task. + # 1. Where the user is already logged in and is confirming a dangerous task. # 2. The user is logging in. # - # This can be detected by the hidden email and password parameter + # The latter can be detected by the hidden email and password parameter - # https://stackoverflow.com/a/574698 + # If we have the email and password variables set then that means we are currently logging in if email && password - # Verify the password again for extra security + # Verify the password if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) From 65c7362607784b49f1906bcb051120743faf2692 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 25 Jul 2023 16:26:35 -0700 Subject: [PATCH 19/22] Update comments --- src/invidious/routes/account.cr | 21 +++++++++++++-------- src/invidious/routes/login.cr | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index c61767fb..d2fd81d7 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -460,16 +460,21 @@ module Invidious::Routes::Account secure = false end - # There are two routes we can go here. - # 1. Where the user is already logged in and is confirming a dangerous task. - # 2. The user is logging in. # - # The latter can be detected by the hidden email and password parameter + # The validate_2fa method is used in two cases: + # 1. To authenticate the user when logging in + # 2. To verify that the user wishes to proceed with a dangerous action. + # + # As we've verified that the totp given is correct we can now proceed with + # authenticating and/or redirecting the user back to where they came from + # - # If we have the email and password variables set then that means we are currently logging in - if email && password - # Verify the password - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) + logging_in = (email && password) + + if logging_in + # Authenticate the user. The rest follows the code in login.cr + if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.not_nil!.byte_slice(0, 55)) + # sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index f50823a0..fdc0ead8 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -59,7 +59,7 @@ module Invidious::Routes::Login if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) # If the password is correct then we'll go ahead and begin 2fa if applicable if user.totp_secret - csrf_token = nil # setting this to false for compatibility reasons. + csrf_token = nil # setting this to nil for compatibility reasons. return templated "user/validate_2fa" end From 7d5e80929cbf64b128bda58ad8179a746021d02d Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 25 Jul 2023 16:27:07 -0700 Subject: [PATCH 20/22] Reduce 2faVerified cookie expire time to 5 minutes --- src/invidious/routes/account.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index d2fd81d7..6091d71c 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -507,9 +507,9 @@ module Invidious::Routes::Account end if CONFIG.domain - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true, path: "/") + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", domain: "#{CONFIG.domain}", value: "1", expires: Time.utc + 5.minutes, secure: secure, http_only: true, path: "/") else - env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 1.hours, secure: secure, http_only: true, path: "/") + env.response.cookies["2faVerified"] = HTTP::Cookie.new(name: "2faVerified", value: "1", expires: Time.utc + 5.minutes, secure: secure, http_only: true, path: "/") end end From bd2e93292b07c60354db228d0e644556108462b8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 25 Jul 2023 16:37:19 -0700 Subject: [PATCH 21/22] Fix lint --- src/invidious/helpers/utils.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index b9bb3121..1f0bc4b0 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -446,7 +446,6 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) return text end - # Templates the 2fa validator page. # # Requires the env, user, sid and locale variables for From 46fef734b470755407fc6e0421ea9b08e05cccc0 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 25 Jul 2023 16:57:40 -0700 Subject: [PATCH 22/22] Specify crotp version in shard.yml --- shard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/shard.yml b/shard.yml index 301f6c3f..a0528066 100644 --- a/shard.yml +++ b/shard.yml @@ -33,6 +33,7 @@ dependencies: version: ~> 0.1.1 crotp: github: philnash/crotp + version: ~> 1.0.0 development_dependencies: spectator: