Skip to content

Instantly share code, notes, and snippets.

@eoinkelly
Last active July 4, 2022 04:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eoinkelly/4603c456566adce9b0932b2f24ead7a2 to your computer and use it in GitHub Desktop.
Save eoinkelly/4603c456566adce9b0932b2f24ead7a2 to your computer and use it in GitHub Desktop.
Helper scripts for testing devise-two-factor 4.x to 5.x
# common helper functions
def replace_line(file_path:, old_line_regex:, new_line:)
new_lines = File.readlines(file_path, chomp: true).map do |line|
if old_line_regex.match?(line)
new_line
else
line
end
end
File.write(file_path, new_lines.join("\n"))
end
#!/usr/bin/env ruby
require_relative "./lib"
def main
app_dir = ARGV.shift
Dir.chdir(app_dir) do
puts "Operating in: #{Dir.pwd}"
# start upgrade to Rails 7
replace_line(file_path: "Gemfile", old_line_regex: /^gem 'rails'/, new_line: "gem 'rails', '~> 7.0'")
system "bundle update rails"
# update devise-two-factor to a version which supports Rails 7
# (must be done because app:update won't run because old devise-two-factor breaks under Rails 7)
replace_line(
file_path: "Gemfile",
old_line_regex: /devise-two-factor/,
new_line: 'gem "devise-two-factor", path: "/Users/eoinkelly/Code/repos/devise-two-factor/devise-two-factor"'
)
# run Rails app:update script and any migrations it creates
system "yes | ./bin/rails app:update"
system "./bin/rails db:migrate"
# Create migration to add the Rails 7 encrypted attribute to the user model
# TODO: change name of model as required
system "./bin/rails g migration AddOtpSecretToUser otp_secret:string"
system "./bin/rails db:migrate"
# create rake task to migrate secret storage
File.write("lib/tasks/devise_two_factor.rake", <<~EOF
namespace :devise_two_factor do
desc "Copy devise_two_factor OTP secret from old format to new format"
task copy_otp_secret_to_rails7_encrypted_attr: [:environment] do
User.find_each do |user|
otp_secret = user.legacy_otp_secret
puts "Processing #{user.email}"
user.update!(otp_secret: otp_secret)
end
end
end
EOF
)
# setup rails encyrpted secrets
system "./bin/rails db:encryption:init"
# the rest has to be done interactively
# ./bin/rails credentials:edit
# Cleanup phase
# #############
#
# create migration to remove the old columns
# ./bin/rails g migration RemoveLegacyDeviseTwoFactorSecretsFromUsers
#
# class RemoveLegacyDeviseTwoFactorSecretsFromUsers < ActiveRecord::Migration[7.0]
# def change
# remove_column :users, :encrypted_otp_secret
# remove_column :users, :encrypted_otp_secret_iv
# remove_column :users, :encrypted_otp_secret_salt
# end
# end
end
end
main
#!/usr/bin/env ruby
require_relative "./lib"
##
# Create a new Rails (6 or 7) app ready to be have devise-two-factor upgraded
#
# Usage
#
# $ ./mk_r6_app
#
rails_version = "_6.1.4.6_"
app_name = "app_rails_#{rails_version == "" ? "7" : "6"}_devise_#{Time.zone.now.strftime("%Y_%m_%d__%H_%M_%S")}"
system "rails #{rails_version} --version"
system "rails #{rails_version} new --no-rc #{app_name}"
Dir.chdir(app_name) do
system "pwd"
system "git add . && git commit -m 'rails #{rails_version} new'"
system "bundle add pry-rails pry-byebug"
system "git add . && git commit -m 'Add handy gems'"
system "bundle exec rails g scaffold User name:string"
system "bundle exec rails db:create db:migrate"
system "git add . && git commit -m 'Create user via scaffold'"
system "bundle add devise"
system "bundle exec rails g devise:install "
system "bundle exec rails g devise User"
system "bundle exec rails db:migrate"
system "bundle exec rails g devise:views" # need views to modify for 2fa
# We want to be able to sign-out without faffing around with JS
replace_line(
file_path: "config/initializers/devise.rb",
old_line_regex: /sign_out_via/,
new_line: "config.sign_out_via = :get"
)
File.write("", <<~EOM
<!DOCTYPE html>
<html>
<head>
<title>Example devise-two-factor app</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
</head>
<body>
<%= link_to("Sign out", destroy_user_session_path) if current_user %>
<%= yield %>
</body>
</html>
EOM
)
system "git add . && git commit -m 'Set up devise'"
system %q(echo 'gem "dotenv-rails", require: "dotenv/rails-now"' >> Gemfile)
system "bundle install"
system "bundle add devise-two-factor"
system "bundle exec rails g devise_two_factor user LEGACY_ENCRYPTION_KEY"
system "bundle exec rails db:migrate"
File.write("app/controllers/application_controller.rb", <<~EOM
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
end
EOM
)
File.write("config/routes.rb", <<~EOF
Rails.application.routes.draw do
devise_for :users
devise_scope :user do
authenticated :user do
resources :users
root to: "users#index"
end
unauthenticated do
root "devise/sessions#new", as: :unauthenticated_root
end
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
EOF
)
File.write("app/views/devise/sessions/new.html.erb", <<~EOF
<h2>Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "current-password" %>
</div>
<% if devise_mapping.rememberable? %>
<div class="field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<% end %>
<div class="form__field">
<%= f.label :otp_attempt, "2FA (Two Factor Auth) code", class: "form__label" %><br />
<%= f.text_field :otp_attempt, class: "form__input" %>
</div>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
EOF
)
File.write(".env", <<~EOM
LEGACY_ENCRYPTION_KEY=68d7574e6b971373fe4cece66edbd76aca11b09057b4b44a0785ee8435d09607bef9c2e0ed6a6a6d9ebfa7e53411a2433c5dc509ca365026670b0d638ddeddc6
EOM
)
system "git add . && git commit -m 'Setup devise-two-factor'"
seed = <<~'EOM'
10.times do |n|
user = User.find_or_create_by(email: "foo-#{n}@example.com") do |user|
puts "Creating User: #{user.email}"
user.password = "blahblahblah"
user.name = "John #{n}Doe"
user.otp_secret = User.generate_otp_secret
user.otp_required_for_login = true
end
end
EOM
File.write("db/seeds.rb", seed)
system "bundle exec rails db:seed"
system "git add . && git commit -m 'Add DB Seeds'"
end
puts "Created '#{app_name}'"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment