diff --git a/assets/js/booru.js b/assets/js/booru.js index 284ee457..289e5278 100644 --- a/assets/js/booru.js +++ b/assets/js/booru.js @@ -99,7 +99,7 @@ function loadBooruData() { initializeFilters(); // CSRF - // TODO + window.booru.csrfToken = $('meta[name="csrf-token"]').content; } function BooruOnRails() { diff --git a/assets/js/ujs.js b/assets/js/ujs.js index a3ae9e3c..23073061 100644 --- a/assets/js/ujs.js +++ b/assets/js/ujs.js @@ -2,8 +2,7 @@ import { $$, makeEl, findFirstTextNode } from './utils/dom'; import { fire, delegate } from './utils/events'; const headers = () => ({ - Accept: 'text/javascript', - 'X-CSRF-Token': window.booru.csrfToken + 'x-csrf-token': window.booru.csrfToken }); function confirm(event, target) { @@ -39,7 +38,7 @@ function linkMethod(event, target) { event.preventDefault(); const form = makeEl('form', { action: target.href, method: 'POST' }); - const csrf = makeEl('input', { type: 'hidden', name: window.booru.csrfParam, value: window.booru.csrfToken }); + const csrf = makeEl('input', { type: 'hidden', name: '_csrf_token', value: window.booru.csrfToken }); const method = makeEl('input', { type: 'hidden', name: '_method', value: target.dataset.method }); document.body.appendChild(form); diff --git a/assets/js/utils/requests.js b/assets/js/utils/requests.js index 3981dd68..d7c597a9 100644 --- a/assets/js/utils/requests.js +++ b/assets/js/utils/requests.js @@ -8,7 +8,7 @@ function fetchJson(verb, endpoint, body) { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': window.booru.csrfToken, + 'x-csrf-token': window.booru.csrfToken }, }; @@ -24,8 +24,7 @@ function fetchHtml(endpoint) { return fetch(endpoint, { credentials: 'same-origin', headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRF-Token': window.booru.csrfToken, + 'x-csrf-token': window.booru.csrfToken }, }); } diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index 8b91316d..54212193 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -145,7 +145,14 @@ defmodule Philomena.Users.User do end def totp_changeset(user, params, backup_codes) do - token = to_string(params["twofactor_token"]) + token = + case params do + %{"user" => %{"twofactor_token" => t}} -> + to_string(t) + + _ -> + "" + end case user.otp_required_for_login do true -> @@ -171,6 +178,33 @@ defmodule Philomena.Users.User do end) end + def totp_qrcode(user) do + secret = totp_secret(user) + provisioning_uri = %URI{ + scheme: "otpauth", + host: "totp", + path: "/Derpibooru:" <> user.email, + query: URI.encode_query(%{ + secret: secret, + issuer: "Derpibooru" + }) + } + + png = + QRCode.to_png(URI.to_string(provisioning_uri)) + |> Base.encode64() + + "data:image/png;base64," <> png + end + + def totp_secret(user) do + Philomena.Users.Encryptor.decrypt_model( + user.encrypted_otp_secret, + user.encrypted_otp_secret_iv, + user.encrypted_otp_secret_salt + ) + end + defp enable_totp_changeset(user, backup_codes) do hashed_codes = @@ -193,20 +227,11 @@ defmodule Philomena.Users.User do end defp totp_valid?(user, token), - do: :pot.valid_totp(token, otp_secret(user), window: 60) + do: :pot.valid_totp(token, totp_secret(user), window: 60) defp backup_code_valid?(user, token), do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1)) defp remove_backup_code(user, token), do: user.otp_backup_codes |> Enum.reject(&Password.verify_pass(token, &1)) - - - defp otp_secret(user) do - Philomena.Users.Encryptor.decrypt_model( - user.encrypted_otp_secret, - user.encrypted_otp_secret_iv, - user.encrypted_otp_secret_salt - ) - end end diff --git a/lib/philomena_web/controllers/registration/totp_controller.ex b/lib/philomena_web/controllers/registration/totp_controller.ex index 34d5035a..ececa83e 100644 --- a/lib/philomena_web/controllers/registration/totp_controller.ex +++ b/lib/philomena_web/controllers/registration/totp_controller.ex @@ -18,20 +18,24 @@ defmodule PhilomenaWeb.Registration.TotpController do _ -> changeset = Pow.Plug.change_user(conn) - render(conn, "edit.html", changeset: changeset) + secret = User.totp_secret(user) + qrcode = User.totp_qrcode(user) + render(conn, "edit.html", changeset: changeset, totp_secret: secret, totp_qrcode: qrcode) end end def update(conn, params) do backup_codes = User.random_backup_codes() + user = Pow.Plug.current_user(conn) - conn - |> Pow.Plug.current_user() + user |> User.totp_changeset(params, backup_codes) |> Repo.update() |> case do {:error, changeset} -> - render(conn, "edit.html", changeset: changeset) + secret = User.totp_secret(user) + qrcode = User.totp_qrcode(user) + render(conn, "edit.html", changeset: changeset, totp_secret: secret, totp_qrcode: qrcode) {:ok, user} -> conn diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 048a765b..b561baff 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -21,6 +21,11 @@ defmodule PhilomenaWeb.Router do plug PhilomenaWeb.Plugs.TotpPlug end + pipeline :protected do + plug Pow.Plug.RequireAuthenticated, + error_handler: Pow.Phoenix.PlugErrorHandler + end + scope "/" do pipe_through [:browser, :ensure_totp] @@ -33,10 +38,12 @@ defmodule PhilomenaWeb.Router do # Additional routes for TOTP scope "/registration", Registration, as: :registration do + pipe_through :protected resources "/totp", TotpController, only: [:edit, :update], singleton: true end scope "/session", Session, as: :session do + pipe_through :protected resources "/totp", TotpController, only: [:new, :create], singleton: true end diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index a18333eb..51751884 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -17,6 +17,7 @@ html lang="en" meta name="theme-color" content="#618fc3" meta name="format-detection" content="telephone=no" meta name="robots" content="noindex, nofollow" + = csrf_meta_tag() script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async" body data-theme="default" = render PhilomenaWeb.LayoutView, "_burger.html", assigns diff --git a/lib/philomena_web/templates/pow/registration/edit.html.eex b/lib/philomena_web/templates/pow/registration/edit.html.eex deleted file mode 100644 index 69130ee0..00000000 --- a/lib/philomena_web/templates/pow/registration/edit.html.eex +++ /dev/null @@ -1,30 +0,0 @@ -

Edit profile

- -<%= form_for @changeset, @action, [as: :user], fn f -> %> - <%= if @changeset.action do %> -
-

Oops, something went wrong! Please check the errors below.

-
- <% end %> - - <%= label f, :current_password %> - <%= password_input f, :current_password %> - <%= error_tag f, :current_password %> - - <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> - - <%= label f, :password %> - <%= password_input f, :password %> - <%= error_tag f, :password %> - - <%= label f, :confirm_password %> - <%= password_input f, :confirm_password %> - <%= error_tag f, :confirm_password %> - -
- <%= submit "Update" %> -
-<% end %> - diff --git a/lib/philomena_web/templates/pow/registration/edit.html.slime b/lib/philomena_web/templates/pow/registration/edit.html.slime new file mode 100644 index 00000000..8802cd8f --- /dev/null +++ b/lib/philomena_web/templates/pow/registration/edit.html.slime @@ -0,0 +1,56 @@ +h1 Account Settings + +p + ' Looking for your content settings? + a<> href="/settings/edit" Click here! + +p + ' Looking for two-factor authentication? + = link "Click here!", to: Routes.registration_totp_path(@conn, :edit) + +h3 API Key +p + ' Your API key is + code + => @current_user.authentication_token + ' - you can use this to allow API consumers to access your account. + +h3 Update Settings +p + strong + ' Don't forget to confirm your changes by entering your current password + ' at the bottom of the page! + += form_for @changeset, @action, [as: :user], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + h3 Email address + .field + = text_input f, :email, class: "input", placeholder: "Email", required: true + = error_tag f, :email + + h3 Change Password + .field + = password_input f, :password, class: "input", placeholder: "New password" + = error_tag f, :password + + .field + = password_input f, :confirm_password, class: "input", placeholder: "Confirm new password" + = error_tag f, :confirm_password + + .fieldlabel + ' Leave these blank if you don't want to change your password. + + br + + .block.block--fixed.block--warning + h3 Confirm + .field + = password_input f, :current_password, class: "input", placeholder: "Current password" + = error_tag f, :current_password + .fieldlabel + ' We need your current password to confirm all of these changes + + = submit "Save Account", class: "button" \ No newline at end of file diff --git a/lib/philomena_web/templates/pow/registration/new.html.eex b/lib/philomena_web/templates/pow/registration/new.html.eex deleted file mode 100644 index decf913b..00000000 --- a/lib/philomena_web/templates/pow/registration/new.html.eex +++ /dev/null @@ -1,28 +0,0 @@ -

Register

- -<%= form_for @changeset, @action, [as: :user], fn f -> %> - <%= if @changeset.action do %> -
-

Oops, something went wrong! Please check the errors below.

-
- <% end %> - - <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> - - <%= label f, :password %> - <%= password_input f, :password %> - <%= error_tag f, :password %> - - <%= label f, :confirm_password %> - <%= password_input f, :confirm_password %> - <%= error_tag f, :confirm_password %> - -
- <%= submit "Register" %> -
-<% end %> - - -<%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %> diff --git a/lib/philomena_web/templates/pow/registration/new.html.slime b/lib/philomena_web/templates/pow/registration/new.html.slime new file mode 100644 index 00000000..163576ad --- /dev/null +++ b/lib/philomena_web/templates/pow/registration/new.html.slime @@ -0,0 +1,46 @@ +h1 Register + += form_for @changeset, @action, [as: :user], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .fieldlabel + ' Non-anonymous posts permanently show your username as of posting time, + ' and may appear on search engines; choose wisely. + .field + = text_input f, :name, class: "input", placeholder: "Username", required: true + = error_tag f, :name + + .fieldlabel + ' You'll use your email address to log in, and we'll use this to get in + ' touch if we need to. Don't worry, we won't share this or spam you. + .field + = text_input f, :email, class: "input", placeholder: "Email", required: true + = error_tag f, :email + + .fieldlabel + ' Pick a good strong password - longer is better! Minimum of 6 characters. + .field + = password_input f, :password, class: "input", placeholder: "Password", required: true + = error_tag f, :password + .field + = password_input f, :confirm_password, class: "input", placeholder: "Confirm password", required: true + = error_tag f, :confirm_password + + br + + .block.block--fixed.block--warning + p + ' We won't share your personal information, won't send you spam emails, + ' and take your security and privacy seriously. + p + strong + ' Don't forget to read the + a<> href="/pages/rules" site rules + ' before you dive in - they contain a quick introduction on how to + ' use the site. + + br + + = submit "Sign Up", class: "button" \ No newline at end of file diff --git a/lib/philomena_web/templates/pow/session/new.html.eex b/lib/philomena_web/templates/pow/session/new.html.eex deleted file mode 100644 index 86c7e7f8..00000000 --- a/lib/philomena_web/templates/pow/session/new.html.eex +++ /dev/null @@ -1,24 +0,0 @@ -

Sign in

- -<%= form_for @changeset, @action, [as: :user], fn f -> %> - <%= if @changeset.action do %> -
-

Oops, something went wrong! Please check the errors below.

-
- <% end %> - - <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> - <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> - - <%= label f, :password %> - <%= password_input f, :password %> - <%= error_tag f, :password %> - -
- <%= submit "Sign in" %> -
-<% end %> - - -<%= link "Register", to: Routes.pow_registration_path(@conn, :new) %> diff --git a/lib/philomena_web/templates/pow/session/new.html.slime b/lib/philomena_web/templates/pow/session/new.html.slime new file mode 100644 index 00000000..24b51147 --- /dev/null +++ b/lib/philomena_web/templates/pow/session/new.html.slime @@ -0,0 +1,28 @@ +h1 Sign in + += form_for @changeset, @action, [as: :user], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + p = link "Forgot your password?", to: Routes.pow_reset_password_reset_password_path(@conn, :new) + + .field + = text_input f, :email, class: "input", required: true, placeholder: "Email" + = error_tag f, :email + + .field + = password_input f, :password, class: "input", required: true, placeholder: "Password" + = error_tag f, :password + + .field + = checkbox f, :persistent_session + = label f, :persistent_session, "Remember me" + + = submit "Sign in", class: "button" + +p + strong + ' Haven't read the + a<> href="/pages/rules" site rules + ' lately? Make sure you read them before posting or editing metadata! \ No newline at end of file diff --git a/lib/philomena_web/templates/registration/totp/edit.html.slime b/lib/philomena_web/templates/registration/totp/edit.html.slime new file mode 100644 index 00000000..71da5060 --- /dev/null +++ b/lib/philomena_web/templates/registration/totp/edit.html.slime @@ -0,0 +1,103 @@ +h1 Two Factor Authentication + += form_for @changeset, Routes.registration_totp_path(@conn, :update), [as: :user], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + = if @current_user.otp_required_for_login do + p + ' Two factor authentication is currently + strong> enabled + ' for your account. + + h4 Enter the generated 6-digit code or one of your backup codes to disable. + p + ' Note that the 6-digit codes are limited to a single use within their + ' lifespan of 30 seconds, so if you just logged in with a code, entering + ' it again here will cause an error. If that's the case, just wait for a + ' new code to be generated. + + .field + = text_input f, :twofactor_token, class: "input", placeholder: "6-digit code" + = error_tag f, :twofactor_token + + - else + p + ' Two factor authentication is currently + strong> disabled + ' for your account. + + br + + .block.block--fixed.block--warning + p + | Enabling 2FA will make it harder for an attacker to get into your account, but it may also make it harder for + strong<> you + | to get into your account. Make sure you'll have access to your authenticator if you enable it. + h4 Download application + p + | You will need an application on your phone that'll generate TOTP codes for you, such as: + ul + li + | Authy ( + a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank" rel="noreferrer" + | Android + | / + a href="https://itunes.apple.com/us/app/authy/id494168017" target="_blank" rel="noreferrer" + | iOS + | ) + li + | LastPass Authenticator ( + a href="https://play.google.com/store/apps/details?id=com.lastpass.authenticator" target="_blank" rel="noreferrer" + | Android + | / + a href="https://itunes.apple.com/us/app/lastpass-authenticator/id1079110004" target="_blank" rel="noreferrer" + | iOS + | / + a href="https://www.microsoft.com/en-us/store/apps/lastpass-authenticator/9nblggh5l9d7" target="_blank" rel="noreferrer" + | Windows Mobile + | ) + li + | Microsoft Authenticator ( + a href="https://play.google.com/store/apps/details?id=com.azure.authenticator" target="_blank" rel="noreferrer" + | Android + | / + a href="https://itunes.apple.com/us/app/microsoft-authenticator/id983156458" target="_blank" rel="noreferrer" + | iOS + | / + a href="https://www.microsoft.com/en-us/store/p/microsoft-authenticator/9nblgggzmcj6" target="_blank" rel="noreferrer" + | Windows Mobile + | ) + ' Google Authenticator is + em> not + ' recommended. + h4 Pair application + p + ' Using the application of your choice, scan the QR code below or enter the following secret key: + strong + = @totp_secret + p + img src=@totp_qrcode alt="QR Code" + h4 Confirm pairing + p Enter the code generated by your authenticator app into the field below for verification. + = text_input f, :twofactor_token, class: "input", placeholder: "6-digit code", autocomplete: "off" + p Note that the 6-digit codes are limited to a single use within their lifespan of 30 seconds, so if you use a code to enable the feature here, you won't be able to immediately use the same code to log in or to disable the feature. You have to wait for a new code to be generated. + .dnp-warning + h4 Warning - Authenticator Backup Codes + p Once you enable 2FA on your account, you will be provided with a list of backup codes that can be used to access your account in the event of you losing access to your authenticator app. You will only be provided with these codes once, so please ensure that you have a way to safely and securely record them before enabling 2FA on your account. If you lose access to your authenticator app and do not have your backup codes, you will be locked out of your account permanently, and we will be unable to assist you. + + br + + .block.block--fixed.block--warning + .field + = password_input f, :current_password, class: "input", placeholder: "Current password" + = error_tag f, :current_password + .fieldlabel + ' We need your current password to confirm these changes + + br + + = submit "Save Account", class: "button" + +p = link "Back", to: Routes.pow_registration_path(@conn, :edit) \ No newline at end of file diff --git a/lib/philomena_web/views/registration/totp_view.ex b/lib/philomena_web/views/registration/totp_view.ex new file mode 100644 index 00000000..2f1125f4 --- /dev/null +++ b/lib/philomena_web/views/registration/totp_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Registration.TotpView do + use PhilomenaWeb, :view +end \ No newline at end of file diff --git a/lib/philomena_web/views/session/totp_view.ex b/lib/philomena_web/views/session/totp_view.ex new file mode 100644 index 00000000..0b899639 --- /dev/null +++ b/lib/philomena_web/views/session/totp_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Session.TotpView do + use PhilomenaWeb, :view +end \ No newline at end of file diff --git a/mix.exs b/mix.exs index 5ce58eda..8db1d8c8 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,8 @@ defmodule Philomena.MixProject do {:nimble_parsec, "~> 0.5.1"}, {:canary, "~> 1.1.1"}, {:scrivener_ecto, "~> 2.0"}, - {:pbkdf2, "~> 2.0"} + {:pbkdf2, "~> 2.0"}, + {:qrcode, "~> 0.1.5"} ] end diff --git a/mix.lock b/mix.lock index 45273bd1..cefdf126 100644 --- a/mix.lock +++ b/mix.lock @@ -41,6 +41,7 @@ "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "pot": {:hex, :pot, "0.10.1", "af7dc220fd45478719b821fb4c1222975132516478483213507f95026298d8ab", [:rebar3], [], "hexpm"}, "pow": {:hex, :pow, "1.0.13", "5ca3e8d9fecca037bfb0ea3b8dde070cc319746498e844d59fc209d461b0d426", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3.0 or ~> 1.4.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, + "qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"}, "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},