commit
f715e8b516
@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
module TwoFactorAuthentication
|
||||||
|
class OtpAuthenticationController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :verify_otp_not_enabled, only: [:show]
|
||||||
|
before_action :require_challenge!, only: [:create]
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
def show
|
||||||
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
session[:new_otp_secret] = User.generate_otp_secret(32)
|
||||||
|
|
||||||
|
redirect_to new_settings_two_factor_authentication_confirmation_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def confirmation_params
|
||||||
|
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_otp_not_enabled
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def acceptable_code?
|
||||||
|
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
|
||||||
|
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,107 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
module TwoFactorAuthentication
|
||||||
|
class WebauthnCredentialsController < BaseController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :require_otp_enabled
|
||||||
|
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
||||||
|
|
||||||
|
def new; end
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def options
|
||||||
|
current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id
|
||||||
|
|
||||||
|
options_for_create = WebAuthn::Credential.options_for_create(
|
||||||
|
user: {
|
||||||
|
name: current_user.account.username,
|
||||||
|
display_name: current_user.account.username,
|
||||||
|
id: current_user.webauthn_id,
|
||||||
|
},
|
||||||
|
exclude: current_user.webauthn_credentials.pluck(:external_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
session[:webauthn_challenge] = options_for_create.challenge
|
||||||
|
|
||||||
|
render json: options_for_create, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
webauthn_credential = WebAuthn::Credential.from_create(params[:credential])
|
||||||
|
|
||||||
|
if webauthn_credential.verify(session[:webauthn_challenge])
|
||||||
|
user_credential = current_user.webauthn_credentials.build(
|
||||||
|
external_id: webauthn_credential.id,
|
||||||
|
public_key: webauthn_credential.public_key,
|
||||||
|
nickname: params[:nickname],
|
||||||
|
sign_count: webauthn_credential.sign_count
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_credential.save
|
||||||
|
flash[:success] = I18n.t('webauthn_credentials.create.success')
|
||||||
|
status = :ok
|
||||||
|
|
||||||
|
if current_user.webauthn_credentials.size == 1
|
||||||
|
UserMailer.webauthn_enabled(current_user).deliver_later!
|
||||||
|
else
|
||||||
|
UserMailer.webauthn_credential_added(current_user, user_credential).deliver_later!
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
||||||
|
status = :internal_server_error
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = t('webauthn_credentials.create.error')
|
||||||
|
status = :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { redirect_path: settings_two_factor_authentication_methods_path }, status: status
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
credential = current_user.webauthn_credentials.find_by(id: params[:id])
|
||||||
|
if credential
|
||||||
|
credential.destroy
|
||||||
|
if credential.destroyed?
|
||||||
|
flash[:success] = I18n.t('webauthn_credentials.destroy.success')
|
||||||
|
|
||||||
|
if current_user.webauthn_credentials.empty?
|
||||||
|
UserMailer.webauthn_disabled(current_user).deliver_later!
|
||||||
|
else
|
||||||
|
UserMailer.webauthn_credential_deleted(current_user, credential).deliver_later!
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.destroy.error')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.destroy.error')
|
||||||
|
end
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_pack
|
||||||
|
use_pack 'auth'
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_otp_enabled
|
||||||
|
unless current_user.otp_enabled?
|
||||||
|
flash[:error] = t('webauthn_credentials.otp_required')
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_webauthn_enabled
|
||||||
|
unless current_user.webauthn_enabled?
|
||||||
|
flash[:error] = t('webauthn_credentials.not_enabled')
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
class TwoFactorAuthenticationMethodsController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :require_challenge!, only: :disable
|
||||||
|
before_action :require_otp_enabled
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
current_user.disable_two_factor!
|
||||||
|
UserMailer.two_factor_disabled(current_user).deliver_later!
|
||||||
|
|
||||||
|
redirect_to settings_otp_authentication_path, flash: { notice: I18n.t('two_factor_authentication.disabled_success') }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_otp_enabled
|
||||||
|
redirect_to settings_otp_authentication_path unless current_user.otp_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,53 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Settings
|
|
||||||
class TwoFactorAuthenticationsController < BaseController
|
|
||||||
include ChallengableConcern
|
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :verify_otp_required, only: [:create]
|
|
||||||
before_action :require_challenge!, only: [:create]
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show
|
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
current_user.otp_secret = User.generate_otp_secret(32)
|
|
||||||
current_user.save!
|
|
||||||
redirect_to new_settings_two_factor_authentication_confirmation_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
if acceptable_code?
|
|
||||||
current_user.otp_required_for_login = false
|
|
||||||
current_user.save!
|
|
||||||
UserMailer.two_factor_disabled(current_user).deliver_later!
|
|
||||||
redirect_to settings_two_factor_authentication_path
|
|
||||||
else
|
|
||||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
|
||||||
render :show
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def confirmation_params
|
|
||||||
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_otp_required
|
|
||||||
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
|
||||||
end
|
|
||||||
|
|
||||||
def acceptable_code?
|
|
||||||
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
|
|
||||||
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,2 @@
|
|||||||
|
import './settings';
|
||||||
|
import './two_factor_authentication';
|
@ -0,0 +1,118 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
|
function getCSRFToken() {
|
||||||
|
var CSRFSelector = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
if (CSRFSelector) {
|
||||||
|
return CSRFSelector.getAttribute('content');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFlashMessages() {
|
||||||
|
Array.from(document.getElementsByClassName('flash-message')).forEach(function(flashMessage) {
|
||||||
|
flashMessage.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(url, body) {
|
||||||
|
axios.post(url, JSON.stringify(body), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRF-Token': getCSRFToken(),
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
}).then(function(response) {
|
||||||
|
window.location.replace(response.data.redirect_path);
|
||||||
|
}).catch(function(error) {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
if (!WebAuthnJSON.supported()) {
|
||||||
|
const unsupported_browser_message = document.getElementById('unsupported-browser-message');
|
||||||
|
if (unsupported_browser_message) {
|
||||||
|
unsupported_browser_message.classList.remove('hidden');
|
||||||
|
document.querySelector('.btn.js-webauthn').disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const webAuthnCredentialRegistrationForm = document.getElementById('new_webauthn_credential');
|
||||||
|
if (webAuthnCredentialRegistrationForm) {
|
||||||
|
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var nickname = event.target.querySelector('input[name="new_webauthn_credential[nickname]"]');
|
||||||
|
if (nickname.value) {
|
||||||
|
axios.get('/settings/security_keys/options')
|
||||||
|
.then((response) => {
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
WebAuthnJSON.create({ 'publicKey': credentialOptions }).then((credential) => {
|
||||||
|
var params = { 'credential': credential, 'nickname': nickname.value };
|
||||||
|
callback('/settings/security_keys', params);
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nickname.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const webAuthnCredentialAuthenticationForm = document.getElementById('webauthn-form');
|
||||||
|
if (webAuthnCredentialAuthenticationForm) {
|
||||||
|
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
axios.get('sessions/security_key_options')
|
||||||
|
.then((response) => {
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
WebAuthnJSON.get({ 'publicKey': credentialOptions }).then((credential) => {
|
||||||
|
var params = { 'user': { 'credential': credential } };
|
||||||
|
callback('sign_in', params);
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const otpAuthenticationForm = document.getElementById('otp-authentication-form');
|
||||||
|
|
||||||
|
const linkToOtp = document.getElementById('link-to-otp');
|
||||||
|
linkToOtp.addEventListener('click', () => {
|
||||||
|
webAuthnCredentialAuthenticationForm.classList.add('hidden');
|
||||||
|
otpAuthenticationForm.classList.remove('hidden');
|
||||||
|
hideFlashMessages();
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkToWebAuthn = document.getElementById('link-to-webauthn');
|
||||||
|
linkToWebAuthn.addEventListener('click', () => {
|
||||||
|
otpAuthenticationForm.classList.add('hidden');
|
||||||
|
webAuthnCredentialAuthenticationForm.classList.remove('hidden');
|
||||||
|
hideFlashMessages();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,69 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::Dereferencer
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
def initialize(uri, permitted_origin: nil, signature_account: nil)
|
||||||
|
@uri = uri
|
||||||
|
@permitted_origin = permitted_origin
|
||||||
|
@signature_account = signature_account
|
||||||
|
end
|
||||||
|
|
||||||
|
def object
|
||||||
|
@object ||= fetch_object!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def bear_cap?
|
||||||
|
@uri.start_with?('bear:')
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_object!
|
||||||
|
if bear_cap?
|
||||||
|
fetch_with_token!
|
||||||
|
else
|
||||||
|
fetch_with_signature!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_with_token!
|
||||||
|
perform_request(bear_cap['u'], headers: { 'Authorization' => "Bearer #{bear_cap['t']}" })
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_with_signature!
|
||||||
|
perform_request(@uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bear_cap
|
||||||
|
@bear_cap ||= Addressable::URI.parse(@uri).query_values
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_request(uri, headers: nil)
|
||||||
|
return if invalid_origin?(uri)
|
||||||
|
|
||||||
|
req = Request.new(:get, uri)
|
||||||
|
|
||||||
|
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||||
|
req.add_headers(headers) if headers
|
||||||
|
req.on_behalf_of(@signature_account) if @signature_account
|
||||||
|
|
||||||
|
req.perform do |res|
|
||||||
|
if res.code == 200
|
||||||
|
json = body_to_json(res.body_with_limit)
|
||||||
|
json if json.present? && json['id'] == uri
|
||||||
|
else
|
||||||
|
raise Mastodon::UnexpectedResponseError, res unless response_successful?(res) || response_error_unsalvageable?(res)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid_origin?(uri)
|
||||||
|
return true if unsupported_uri_scheme?(uri)
|
||||||
|
|
||||||
|
needle = Addressable::URI.parse(uri).host
|
||||||
|
haystack = Addressable::URI.parse(@permitted_origin).host
|
||||||
|
|
||||||
|
!haystack.casecmp(needle).zero?
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: webauthn_credentials
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# external_id :string not null
|
||||||
|
# public_key :string not null
|
||||||
|
# nickname :string not null
|
||||||
|
# sign_count :bigint(8) default(0), not null
|
||||||
|
# user_id :bigint(8)
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebauthnCredential < ApplicationRecord
|
||||||
|
validates :external_id, :public_key, :nickname, :sign_count, presence: true
|
||||||
|
validates :external_id, uniqueness: true
|
||||||
|
validates :nickname, uniqueness: { scope: :user_id }
|
||||||
|
validates :sign_count,
|
||||||
|
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
|
||||||
|
end
|
@ -1,14 +1,7 @@
|
|||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.login')
|
= t('auth.login')
|
||||||
|
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
- if @webauthn_enabled
|
||||||
%p.hint.otp-hint= t('simple_form.hints.sessions.otp')
|
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
|
||||||
|
|
||||||
.fields-group
|
= render partial: 'auth/sessions/two_factor/otp_authentication_form', locals: { hidden: @scheme_type != 'totp' }
|
||||||
= f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true
|
|
||||||
|
|
||||||
.actions
|
|
||||||
= f.button :button, t('auth.login'), type: :submit
|
|
||||||
|
|
||||||
- if Setting.site_contact_email.present?
|
|
||||||
%p.hint.subtle-hint= t('users.otp_lost_help_html', email: mail_to(Setting.site_contact_email, nil))
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
= simple_form_for(resource,
|
||||||
|
as: resource_name,
|
||||||
|
url: session_path(resource_name),
|
||||||
|
html: { method: :post, id: 'otp-authentication-form' }.merge(hidden ? { class: 'hidden' } : {})) do |f|
|
||||||
|
%p.hint.authentication-hint= t('simple_form.hints.sessions.otp')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
||||||
|
- if Setting.site_contact_email.present?
|
||||||
|
%p.hint.subtle-hint= t('users.otp_lost_help_html', email: mail_to(Setting.site_contact_email, nil))
|
||||||
|
|
||||||
|
- if @webauthn_enabled
|
||||||
|
.form-footer
|
||||||
|
= link_to(t('auth.link_to_webauth'), '#', id: 'link-to-webauthn')
|
@ -0,0 +1,17 @@
|
|||||||
|
%p.flash-message.hidden#unsupported-browser-message= t 'webauthn_credentials.not_supported'
|
||||||
|
%p.flash-message.alert.hidden#security-key-error-message= t 'webauthn_credentials.invalid_credential'
|
||||||
|
|
||||||
|
|
||||||
|
= simple_form_for(resource,
|
||||||
|
as: resource_name,
|
||||||
|
url: session_path(resource_name),
|
||||||
|
html: { method: :post, id: 'webauthn-form' }.merge(hidden ? { class: 'hidden' } : {})) do |f|
|
||||||
|
%h3.title= t('simple_form.title.sessions.webauthn')
|
||||||
|
%p.hint= t('simple_form.hints.sessions.webauthn')
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('auth.use_security_key'), class: 'js-webauthn', type: :submit
|
||||||
|
|
||||||
|
.form-footer
|
||||||
|
%p= t('auth.dont_have_your_security_key')
|
||||||
|
= link_to(t('auth.link_to_otp'), '#', id: 'link-to-otp')
|
@ -0,0 +1,9 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.two_factor_authentication')
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
%p.hint= t('otp_authentication.description_html')
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
= link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'block-button'
|
@ -0,0 +1,17 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.webauthn_authentication')
|
||||||
|
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%tbody
|
||||||
|
- current_user.webauthn_credentials.each do |credential|
|
||||||
|
%tr
|
||||||
|
%td= credential.nickname
|
||||||
|
%td= t('webauthn_credentials.registered_on', date: l(credential.created_at.to_date, format: :with_month_name))
|
||||||
|
%td
|
||||||
|
= table_link_to 'trash', t('webauthn_credentials.delete'), settings_webauthn_credential_path(credential.id), method: :delete, data: { confirm: t('webauthn_credentials.delete_confirmation') }
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
= link_to t('webauthn_credentials.add'), new_settings_webauthn_credential_path, class: 'block-button'
|
@ -0,0 +1,14 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.webauthn_authentication')
|
||||||
|
|
||||||
|
= simple_form_for(:new_webauthn_credential, url: settings_webauthn_credentials_path, html: { id: :new_webauthn_credential }) do |f|
|
||||||
|
%p.flash-message.hidden#unsupported-browser-message= t 'webauthn_credentials.not_supported'
|
||||||
|
%p.flash-message.alert.hidden#security-key-error-message= t 'webauthn_credentials.invalid_credential'
|
||||||
|
|
||||||
|
%p.hint= t('webauthn_credentials.description_html')
|
||||||
|
|
||||||
|
.fields_group
|
||||||
|
= f.input :nickname, wrapper: :with_block_label, hint: t('webauthn_credentials.nickname_hint'), input_html: { :autocomplete => 'off' }, required: true
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit
|
@ -0,0 +1,41 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.two_factor_authentication')
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
= link_to t('two_factor_authentication.disable'), disable_settings_two_factor_authentication_methods_path, class: 'button button--destructive', method: :post
|
||||||
|
|
||||||
|
%p.hint
|
||||||
|
%span.positive-hint
|
||||||
|
= fa_icon 'check'
|
||||||
|
= ' '
|
||||||
|
= t 'two_factor_authentication.enabled'
|
||||||
|
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('two_factor_authentication.methods')
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td= t('two_factor_authentication.otp')
|
||||||
|
%td
|
||||||
|
= table_link_to 'pencil', t('two_factor_authentication.edit'), settings_otp_authentication_path, method: :post
|
||||||
|
%tr
|
||||||
|
%td= t('two_factor_authentication.webauthn')
|
||||||
|
- if current_user.webauthn_enabled?
|
||||||
|
%td
|
||||||
|
= table_link_to 'pencil', t('two_factor_authentication.edit'), settings_webauthn_credentials_path, method: :get
|
||||||
|
- else
|
||||||
|
%td
|
||||||
|
= table_link_to 'key', t('two_factor_authentication.add'), new_settings_webauthn_credential_path, method: :get
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
%h3= t('two_factor_authentication.recovery_codes')
|
||||||
|
%p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
|
@ -1,36 +0,0 @@
|
|||||||
- content_for :page_title do
|
|
||||||
= t('settings.two_factor_authentication')
|
|
||||||
|
|
||||||
- if current_user.otp_required_for_login
|
|
||||||
%p.hint
|
|
||||||
%span.positive-hint
|
|
||||||
= fa_icon 'check'
|
|
||||||
= ' '
|
|
||||||
= t 'two_factor_authentication.enabled'
|
|
||||||
|
|
||||||
%hr.spacer/
|
|
||||||
|
|
||||||
= simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
|
|
||||||
.fields-group
|
|
||||||
= f.input :otp_attempt, wrapper: :with_block_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
|
|
||||||
|
|
||||||
.actions
|
|
||||||
= f.button :button, t('two_factor_authentication.disable'), type: :submit, class: 'negative'
|
|
||||||
|
|
||||||
%hr.spacer/
|
|
||||||
|
|
||||||
%h3= t('two_factor_authentication.recovery_codes')
|
|
||||||
%p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
|
|
||||||
|
|
||||||
%hr.spacer/
|
|
||||||
|
|
||||||
.simple_form
|
|
||||||
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
|
|
||||||
|
|
||||||
- else
|
|
||||||
.simple_form
|
|
||||||
%p.hint= t('two_factor_authentication.description_html')
|
|
||||||
|
|
||||||
%hr.spacer/
|
|
||||||
|
|
||||||
= link_to t('two_factor_authentication.setup'), settings_two_factor_authentication_path, data: { method: :post }, class: 'block-button'
|
|
@ -0,0 +1,44 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.webauthn_credential.added.title'
|
||||||
|
%p.lead= "#{t 'devise.mailer.webauthn_credential.added.explanation' }:"
|
||||||
|
%p.lead= @webauthn_credential.nickname
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t('settings.account_settings')
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'devise.mailer.two_factor_enabled.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.two_factor_enabled.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,44 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.webauthn_credential.deleted.title'
|
||||||
|
%p.lead= "#{t 'devise.mailer.webauthn_credential.deleted.explanation' }:"
|
||||||
|
%p.lead= @webauthn_credential.nickname
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t('settings.account_settings')
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'devise.mailer.webauthn_credential.deleted.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.webauthn_credential.deleted.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,43 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.webauthn_disabled.title'
|
||||||
|
%p.lead= t 'devise.mailer.webauthn_disabled.explanation'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t('settings.account_settings')
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'devise.mailer.webauthn_disabled.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.webauthn_disabled.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,43 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.webauthn_enabled.title'
|
||||||
|
%p.lead= t 'devise.mailer.webauthn_enabled.explanation'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t('settings.account_settings')
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'devise.mailer.webauthn_credentia.added.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.webauthn_credentia.added.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ "$RUN_STREAMING" != "true" ]; then BIND=0.0.0.0 bundle exec puma -C config/puma.rb; else BIND=0.0.0.0 node ./streaming; fi
|
@ -0,0 +1,24 @@
|
|||||||
|
WebAuthn.configure do |config|
|
||||||
|
# This value needs to match `window.location.origin` evaluated by
|
||||||
|
# the User Agent during registration and authentication ceremonies.
|
||||||
|
config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}"
|
||||||
|
|
||||||
|
# Relying Party name for display purposes
|
||||||
|
config.rp_name = "Mastodon"
|
||||||
|
|
||||||
|
# Optionally configure a client timeout hint, in milliseconds.
|
||||||
|
# This hint specifies how long the browser should wait for an
|
||||||
|
# attestation or an assertion response.
|
||||||
|
# This hint may be overridden by the browser.
|
||||||
|
# https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
|
||||||
|
config.credential_options_timeout = 120_000
|
||||||
|
|
||||||
|
# You can optionally specify a different Relying Party ID
|
||||||
|
# (https://www.w3.org/TR/webauthn/#relying-party-identifier)
|
||||||
|
# if it differs from the default one.
|
||||||
|
#
|
||||||
|
# In this case the default would be "auth.example.com", but you can set it to
|
||||||
|
# the suffix "example.com"
|
||||||
|
#
|
||||||
|
# config.rp_id = "example.com"
|
||||||
|
end
|
@ -0,0 +1,16 @@
|
|||||||
|
class CreateWebauthnCredentials < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :webauthn_credentials do |t|
|
||||||
|
t.string :external_id, null: false
|
||||||
|
t.string :public_key, null: false
|
||||||
|
t.string :nickname, null: false
|
||||||
|
t.bigint :sign_count, null: false, default: 0
|
||||||
|
|
||||||
|
t.index :external_id, unique: true
|
||||||
|
|
||||||
|
t.references :user, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddWebauthnIdToUsers < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :users, :webauthn_id, :string
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue