Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ ENJU_LEAF_DEFAULT_LOCALE=ja
ENJU_LEAF_TIME_ZONE=Asia/Tokyo
# ENJU_LEAF_STORAGE_BUCKET=enju-leaf
# ENJU_LEAF_STORAGE_ENDPOINT=http://minio:9000
ENJU_LEAF_2FA_ENCRYPTION_KEY=
ENJU_LEAF_ACTION_MAILER_DELIVERY_METHOD=test
ENJU_LEAF_EXTRACT_TEXT=
ENJU_LEAF_EXTRACT_FILESIZE_LIMIT=2097152
# ENJU_LEAF_RESOURCESYNC_BASE_URL=http://localhost:8080
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/rubyonrails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
POSTGRES_USER: rails
POSTGRES_PASSWORD: password
CC_TEST_REPORTER_ID: c193cb8ea058a7d62fd62d6d05adaaf95f6bdf882c1039500b30b54494a36e52
ENJU_LEAF_2FA_ENCRYPTION_KEY: 64aba18129eb855d71a785b0aca726dd5eb8f4104cc779e97f2868d9a8cf796105f4599d9320793a5dfb58cc3ccbe93b293d5db1a12cba05ab79d15d5710cb51
SECRET_KEY_BASE: 4a54f07a6c5e604edac920225ab6f4d9a919edf8597f61ff85dbce0b3ab64433de86a54c192df1e242022c64108923d5382705a1e221c1ca39b79d902824d948
NODE_OPTIONS: --openssl-legacy-provider
steps:
- name: Checkout code
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ gem 'kramdown'
gem 'solid_queue'
gem 'mission_control-jobs'
gem 'acts-as-taggable-on'
gem 'devise-two-factor'
gem 'rqrcode'
gem 'resync' # , github: 'nabeta/resync', branch: 'add-datetime'
gem 'pretender'
gem 'caxlsx'
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ GEM
caxlsx_rails (0.6.4)
actionpack (>= 3.1)
caxlsx (>= 3.0)
chunky_png (1.4.0)
climate_control (1.2.0)
cocoon (1.2.15)
concurrent-ruby (1.3.4)
Expand All @@ -142,6 +143,11 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (5.1.0)
activesupport (~> 7.0)
devise (~> 4.0)
railties (~> 7.0)
rotp (~> 6.0)
diff-lcs (1.5.1)
docile (1.4.1)
dotenv (3.1.2)
Expand Down Expand Up @@ -390,6 +396,11 @@ GEM
xml-mapping_extensions (~> 0.4, >= 0.4.8)
rexml (3.3.5)
strscan
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rsolr (2.6.0)
builder (>= 2.1.2)
faraday (>= 0.9, < 3, != 2.0.0)
Expand Down Expand Up @@ -538,6 +549,7 @@ DEPENDENCIES
date_validator
debug
devise
devise-two-factor
dotenv-rails
factory_bot_rails (~> 6.4.0)
faraday-multipart
Expand Down Expand Up @@ -570,6 +582,7 @@ DEPENDENCIES
rdf-vocab
redis (>= 4.0.1)
resync
rqrcode
rspec-rails
rspec_junit_formatter
rss
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ class ApplicationController < ActionController::Base
include EnjuEvent::Controller
include EnjuSubject::Controller
include Pundit::Authorization
before_action :configure_permitted_parameters, if: :devise_controller?
after_action :verify_authorized, unless: :devise_controller?
impersonates :user

protected

def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
end
13 changes: 13 additions & 0 deletions app/controllers/otp_secrets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class OtpSecretsController < ApplicationController
before_action :skip_authorization

def create
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(
otp_required_for_login: true
)
end

redirect_to two_factor_authentication_url
end
end
22 changes: 22 additions & 0 deletions app/controllers/two_factor_authentications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class TwoFactorAuthenticationsController < ApplicationController
before_action :skip_authorization

def show
end

def create
current_user.update!(
otp_secret: User.generate_otp_secret
)

redirect_to two_factor_authentication_url
end

def destroy
current_user.update!(
otp_secret: nil,
otp_required_for_login: false
)
redirect_to two_factor_authentication_url
end
end
10 changes: 10 additions & 0 deletions app/helpers/my_account_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module MyAccountHelper
def otp_barcode(user = current_user, issuer = @library_group.display_name.localize)
qrcode = RQRCode::QRCode.new(current_user.otp_provisioning_uri(user.username, issuer: issuer))
image_tag("data:image/png;base64,#{Base64.encode64(qrcode.as_png(
bit_depth: 1,
size: 240,
module_px_size: 12
).to_s)}")
end
end
4 changes: 2 additions & 2 deletions app/models/concerns/enju_seed/enju_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ module EnjuUser
with_options if: :password_required? do |v|
v.validates_presence_of :password
v.validates_confirmation_of :password
v.validates_length_of :password, allow_blank: true,
within: Devise::password_length
end
validates_length_of :password, allow_blank: true,
within: Devise::password_length

before_validation :set_lock_information
before_destroy :check_role_before_destroy
Expand Down
39 changes: 24 additions & 15 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
class User < ApplicationRecord
devise :two_factor_authenticatable, :two_factor_backupable,
:otp_secret_encryption_key => ENV['ENJU_LEAF_2FA_ENCRYPTION_KEY']

# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
devise :registerable,
:recoverable, :rememberable, :lockable
include EnjuSeed::EnjuUser
include EnjuCirculation::EnjuUser
Expand All @@ -14,18 +17,24 @@ class User < ApplicationRecord
#
# Table name: users
#
# id :bigint not null, primary key
# email :string default(""), not null
# encrypted_password :string default(""), not null
# reset_password_token :string
# reset_password_sent_at :datetime
# remember_created_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# username :string
# expired_at :datetime
# failed_attempts :integer default(0)
# unlock_token :string
# locked_at :datetime
# confirmed_at :datetime
# id :bigint not null, primary key
# email :string default(""), not null
# encrypted_password :string default(""), not null
# reset_password_token :string
# reset_password_sent_at :datetime
# remember_created_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# username :string
# expired_at :datetime
# failed_attempts :integer default(0)
# unlock_token :string
# locked_at :datetime
# confirmed_at :datetime
# encrypted_otp_secret :string
# encrypted_otp_secret_iv :string
# encrypted_otp_secret_salt :string
# consumed_timestep :integer
# otp_required_for_login :boolean default(FALSE), not null
# otp_backup_codes :string is an Array
#
5 changes: 5 additions & 0 deletions app/views/devise/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<%= f.password_field :password, autocomplete: "off" %>
</div>

<div class="field">
<%= f.label :otp_attempt %><br />
<%= f.password_field :otp_attempt, autocomplete: "off" %>
</div>

<% if devise_mapping.rememberable? -%>
<div class="field">
<%= f.check_box :remember_me %>
Expand Down
19 changes: 19 additions & 0 deletions app/views/layouts/two_factor_authentications.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%= @locale.to_s -%>" lang="<%= @locale.to_s -%>">
<head>
<meta charset="UTF-8" />
<%= render 'page/include' %>
<title><%= t('page.two_factor_authentication') %></title>
</head>

<%= render 'page/header' %>
<%= render 'page/menu' %>

<div id="content">
<%= yield %>
</div>

<%= render 'page/footer' %>

</body>
</html>
1 change: 1 addition & 0 deletions app/views/my_accounts/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ul>
<li><%= link_to t('page.edit'), edit_my_account_path -%></li>
<li><%= link_to t('activerecord.models.registration'), edit_user_registration_path -%></li>
<li><%= link_to t('page.two_factor_authentication'), two_factor_authentication_path -%></li>
<%- if policy(@profile).destroy? -%>
<li><%= link_to t('page.destroy'), @profile, data: {confirm: t('page.are_you_sure')}, method: :delete -%></li>
<%- end -%>
Expand Down
18 changes: 18 additions & 0 deletions app/views/two_factor_authentications/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div id="content_detail" class="ui-corner-all ui-widget-content">
<h1 class="title"><%= t('page.two_factor_authentication') -%></h1>
<div id="content_list">
<% if current_user.otp_secret %>
<div><%= otp_barcode %></div>
<%= button_to 'Disable', two_factor_authentication_path, method: :delete %>
<% else %>
<div><%= otp_barcode %></div>
<%= form_with url: two_factor_authentication_path, method: :post do |f| %>
<%= f.text_field :otp_attempt %>
<%= f.submit 'Enable' %>
<% end %>
<% end %>
</div>
</div>

<div id="submenu" class="ui-corner-all ui-widget-content">
</div>
29 changes: 29 additions & 0 deletions app/views/two_factor_authentications/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div id="content_detail" class="ui-corner-all ui-widget-content">
<h1 class="title"><%= t('page.two_factor_authentication') -%></h1>
<div id="content_list">
<% if current_user.otp_secret %>
<% if current_user.otp_required_for_login %>
<p><%= t('two_factor_authentication.already_enabled') %></p>
<% else %>
<div><%= otp_barcode %></div>

<%= form_with url: otp_secret_path, method: :post do |f| %>
<%= f.text_field :otp_attempt %>
<%= f.submit t('two_factor_authentication.verify') %>
<% end %>
<% end %>
<% end %>
</div>
</div>

<div id="submenu" class="ui-corner-all ui-widget-content">
<ul>
<% if current_user.otp_required_for_login %>
<li><%= link_to t('two_factor_authentication.disable'), two_factor_authentication_path, method: :delete, data: {confirm: t('page.are_you_sure')} %></li>
<% elsif current_user.otp_secret %>
<li><%= link_to t('two_factor_authentication.disable'), two_factor_authentication_path, method: :delete, data: {confirm: t('page.are_you_sure')} %></li>
<% else %>
<li><%= link_to t('two_factor_authentication.enable'), two_factor_authentication_path, method: :post, data: {confirm: t('page.are_you_sure')} %></li>
<% end %>
</ul>
</div>
5 changes: 5 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :two_factor_authenticatable
manager.default_strategies(:scope => :user).unshift :two_factor_backupable
end

# The secret key used by Devise. Devise uses this key to generate
# random tokens. Changing this key will render invalid all existing
# confirmation, reset password and unlock tokens in the database.
Expand Down
7 changes: 7 additions & 0 deletions config/locales/enju_leaf_en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ en:
auto_generated_password: Set auto-generated password
friendly_id: Username
remember_me: "Remember me"
otp_attempt: Authentication code
role:
name: Name
display_name: Display name
Expand Down Expand Up @@ -296,6 +297,7 @@ en:
delegated: Delegated
impersonate: "Impersonate"
stop_impersonating: "Back to %{username}"
two_factor_authentication: Two factor authentication
title:
index: "index"
show: "show"
Expand Down Expand Up @@ -406,3 +408,8 @@ en:
no_number: No number
iiif:
open_in_new_window: Open in new window
two_factor_authentication:
enable: Enable
disable: Disable
verify: Verify
already_enabled: Two factor authentication is enabled.
7 changes: 7 additions & 0 deletions config/locales/enju_leaf_ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ja:
auto_generated_password: パスワードの自動生成
friendly_id: ユーザ名
remember_me: "ウインドウを閉じてもログインしたままにする"
otp_attempt: 2要素認証コード
role:
name: 名前
display_name: 表示名
Expand Down Expand Up @@ -281,6 +282,7 @@ ja:
delegated: 代理
impersonate: "ログインユーザを切り替える"
stop_impersonating: "%{username}に戻る"
two_factor_authentication: 2要素認証
title:
index: "一覧"
show: "表示"
Expand Down Expand Up @@ -388,3 +390,8 @@ ja:
no_number: 番号なし
iiif:
open_in_new_window: 新しいウインドウで開く
two_factor_authentication:
enable: 有効化
disable: 無効化
verify: 確認
already_enabled: 2要素認証は有効になっています。
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@
resources :order_lists
end

resource :two_factor_authentication, only: [:show, :create, :destroy]
resource :otp_secret, only: :create

devise_for :users

get '/page/about' => 'page#about'
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20220319174806_add_devise_two_factor_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :encrypted_otp_secret, :string
add_column :users, :encrypted_otp_secret_iv, :string
add_column :users, :encrypted_otp_secret_salt, :string
add_column :users, :consumed_timestep, :integer
add_column :users, :otp_required_for_login, :boolean, default: false, null: false
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :otp_backup_codes, :string, array: true
end
end
6 changes: 6 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,12 @@
t.string "unlock_token"
t.datetime "locked_at", precision: nil
t.datetime "confirmed_at", precision: nil
t.string "encrypted_otp_secret"
t.string "encrypted_otp_secret_iv"
t.string "encrypted_otp_secret_salt"
t.integer "consumed_timestep"
t.boolean "otp_required_for_login", default: false, null: false
t.string "otp_backup_codes", array: true
t.index ["email"], name: "index_users_on_email"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
Expand Down
Loading