add otp views

This commit is contained in:
byte[] 2019-11-12 23:49:37 -05:00
parent f1726e3d52
commit f627677cb6
18 changed files with 299 additions and 105 deletions

View file

@ -99,7 +99,7 @@ function loadBooruData() {
initializeFilters(); initializeFilters();
// CSRF // CSRF
// TODO window.booru.csrfToken = $('meta[name="csrf-token"]').content;
} }
function BooruOnRails() { function BooruOnRails() {

View file

@ -2,8 +2,7 @@ import { $$, makeEl, findFirstTextNode } from './utils/dom';
import { fire, delegate } from './utils/events'; import { fire, delegate } from './utils/events';
const headers = () => ({ const headers = () => ({
Accept: 'text/javascript', 'x-csrf-token': window.booru.csrfToken
'X-CSRF-Token': window.booru.csrfToken
}); });
function confirm(event, target) { function confirm(event, target) {
@ -39,7 +38,7 @@ function linkMethod(event, target) {
event.preventDefault(); event.preventDefault();
const form = makeEl('form', { action: target.href, method: 'POST' }); 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 }); const method = makeEl('input', { type: 'hidden', name: '_method', value: target.dataset.method });
document.body.appendChild(form); document.body.appendChild(form);

View file

@ -8,7 +8,7 @@ function fetchJson(verb, endpoint, body) {
credentials: 'same-origin', credentials: 'same-origin',
headers: { headers: {
'Content-Type': 'application/json', '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, { return fetch(endpoint, {
credentials: 'same-origin', credentials: 'same-origin',
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'x-csrf-token': window.booru.csrfToken
'X-CSRF-Token': window.booru.csrfToken,
}, },
}); });
} }

View file

@ -145,7 +145,14 @@ defmodule Philomena.Users.User do
end end
def totp_changeset(user, params, backup_codes) do 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 case user.otp_required_for_login do
true -> true ->
@ -171,6 +178,33 @@ defmodule Philomena.Users.User do
end) end)
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 defp enable_totp_changeset(user, backup_codes) do
hashed_codes = hashed_codes =
@ -193,20 +227,11 @@ defmodule Philomena.Users.User do
end end
defp totp_valid?(user, token), 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), defp backup_code_valid?(user, token),
do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1)) do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1))
defp remove_backup_code(user, token), defp remove_backup_code(user, token),
do: user.otp_backup_codes |> Enum.reject(&Password.verify_pass(token, &1)) 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 end

View file

@ -18,20 +18,24 @@ defmodule PhilomenaWeb.Registration.TotpController do
_ -> _ ->
changeset = Pow.Plug.change_user(conn) 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
end end
def update(conn, params) do def update(conn, params) do
backup_codes = User.random_backup_codes() backup_codes = User.random_backup_codes()
user = Pow.Plug.current_user(conn)
conn user
|> Pow.Plug.current_user()
|> User.totp_changeset(params, backup_codes) |> User.totp_changeset(params, backup_codes)
|> Repo.update() |> Repo.update()
|> case do |> case do
{:error, changeset} -> {: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} -> {:ok, user} ->
conn conn

View file

@ -21,6 +21,11 @@ defmodule PhilomenaWeb.Router do
plug PhilomenaWeb.Plugs.TotpPlug plug PhilomenaWeb.Plugs.TotpPlug
end end
pipeline :protected do
plug Pow.Plug.RequireAuthenticated,
error_handler: Pow.Phoenix.PlugErrorHandler
end
scope "/" do scope "/" do
pipe_through [:browser, :ensure_totp] pipe_through [:browser, :ensure_totp]
@ -33,10 +38,12 @@ defmodule PhilomenaWeb.Router do
# Additional routes for TOTP # Additional routes for TOTP
scope "/registration", Registration, as: :registration do scope "/registration", Registration, as: :registration do
pipe_through :protected
resources "/totp", TotpController, only: [:edit, :update], singleton: true resources "/totp", TotpController, only: [:edit, :update], singleton: true
end end
scope "/session", Session, as: :session do scope "/session", Session, as: :session do
pipe_through :protected
resources "/totp", TotpController, only: [:new, :create], singleton: true resources "/totp", TotpController, only: [:new, :create], singleton: true
end end

View file

@ -17,6 +17,7 @@ html lang="en"
meta name="theme-color" content="#618fc3" meta name="theme-color" content="#618fc3"
meta name="format-detection" content="telephone=no" meta name="format-detection" content="telephone=no"
meta name="robots" content="noindex, nofollow" meta name="robots" content="noindex, nofollow"
= csrf_meta_tag()
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async" script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
body data-theme="default" body data-theme="default"
= render PhilomenaWeb.LayoutView, "_burger.html", assigns = render PhilomenaWeb.LayoutView, "_burger.html", assigns

View file

@ -1,30 +0,0 @@
<h1>Edit profile</h1>
<%= form_for @changeset, @action, [as: :user], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% 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 %>
<div>
<%= submit "Update" %>
</div>
<% end %>

View file

@ -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"

View file

@ -1,28 +0,0 @@
<h1>Register</h1>
<%= form_for @changeset, @action, [as: :user], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% 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 %>
<div>
<%= submit "Register" %>
</div>
<% end %>
<span><%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %></span>

View file

@ -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"

View file

@ -1,24 +0,0 @@
<h1>Sign in</h1>
<%= form_for @changeset, @action, [as: :user], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% 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 %>
<div>
<%= submit "Sign in" %>
</div>
<% end %>
<span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span>

View file

@ -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!

View file

@ -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)

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Registration.TotpView do
use PhilomenaWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.Session.TotpView do
use PhilomenaWeb, :view
end

View file

@ -53,7 +53,8 @@ defmodule Philomena.MixProject do
{:nimble_parsec, "~> 0.5.1"}, {:nimble_parsec, "~> 0.5.1"},
{:canary, "~> 1.1.1"}, {:canary, "~> 1.1.1"},
{:scrivener_ecto, "~> 2.0"}, {:scrivener_ecto, "~> 2.0"},
{:pbkdf2, "~> 2.0"} {:pbkdf2, "~> 2.0"},
{:qrcode, "~> 0.1.5"}
] ]
end end

View file

@ -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"}, "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"}, "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"}, "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"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"}, "retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"},
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"}, "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},