1

I have a two-factor verification page, a secret key(Ciphertext) is displayed on it and I already have clipboard.js installed in my application.

enter image description here

I wonder how it is possible to create a button to copy that secret key?

= simple_form_for @google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
  h4 = t('.step-1')
  p
    span = t('.download-app')
    span == t('.guide-link')

  h4 = t('.step-2')
  p: span = t('.scan-qr-code')

  = f.input :uri do
    = qr_tag(@google_auth.uri)

  = f.input :otp_secret do
    .input-group
      = f.input_field :otp_secret, class: 'upcase', readonly: true
      span.input-group-btn
        a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
          i.fa.fa-refresh

  h4 = t('.step-3')
  p: span = t('.enter-passcode')

  = f.input :otp

  hr.split
  = f.button :wrapped, t('.submit'), cancel: settings_path

= content_for :guide do
  ul.list-unstyled
    li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
      i.fa.fa-apple
      span = t('.ios')
    li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
      i.fa.fa-android
      span = t('.android')

I tried to do like this, but it didn't work:

a.btn.btn-default data-clipboard-action='copy' data-clipboard-target=':otp_secret'
  i.fa.fa-clipboard

In the example above it is copying only the pure otp_secret text.

spec\models\two_factor\app_spec.rb:

require 'spec_helper'

describe TwoFactor::App do
  let(:member) { create :member }
  let(:app) { member.app_two_factor  }

  describe "generate code" do
    subject { app }

    its(:otp_secret) { should_not be_blank }
  end

  describe '#refresh' do
    context 'inactivated' do
      it {
        orig_otp_secret = app.otp_secret.dup
        app.refresh!
        expect(app.otp_secret).not_to eq(orig_otp_secret)
      }
    end

    context 'activated' do
      subject { create :two_factor_app, activated: true }

      it {
        orig_otp_secret = subject.otp_secret.dup
        subject.refresh!
        expect(subject.otp_secret).to eq(orig_otp_secret)
      }
    end
  end

  describe 'uniq validate' do
    let(:member) { create :member }

    it "reject duplicate creation" do
      duplicate = TwoFactor.new app.attributes
      expect(duplicate).not_to be_valid
    end
  end

  describe 'self.fetch_by_type' do
    it "return nil for wrong type" do
      expect(TwoFactor.by_type(:foobar)).to be_nil
    end

    it "create new one by type" do
      expect {
        expect(app).not_to be_nil
      }.to change(TwoFactor::App, :count).by(1)
    end

    it "retrieve exist one instead of creating" do
      two_factor = member.app_two_factor
      expect(member.app_two_factor).to eq(two_factor)
    end
  end

  describe '#active!' do
    subject { member.app_two_factor }
    before { subject.active! }

    its(:activated?) { should be_true }
  end

  describe '#deactive!' do
    subject { create :two_factor_app, activated: true }
    before { subject.deactive! }

    its(:activated?) { should_not be_true }
  end


  describe '.activated' do
    before { create :member, :app_two_factor_activated }

    it "should has activated" do
      expect(TwoFactor.activated?).to be_true
    end
  end

  describe 'send_notification_mail' do
    let(:mail) { ActionMailer::Base.deliveries.last }

    describe "activated" do
      before { app.active! }

      it { expect(mail.subject).to match('Google authenticator activated') }
    end

    describe "deactived" do
      let(:member) { create :member, :app_two_factor_activated }
      before { app.deactive! }

      it { expect(mail.subject).to match('Google authenticator deactivated') }
    end
  end

end

app.rb:

class TwoFactor::App < ::TwoFactor

  def verify?
    return false if otp_secret.blank?

    rotp = ROTP::TOTP.new(otp_secret)

    if rotp.verify(otp)
      touch(:last_verify_at)
      true
    else
      errors.add :otp, :invalid
      false
    end
  end

  def uri
    totp = ROTP::TOTP.new(otp_secret)
    totp.provisioning_uri(member.email) + "&issuer=#{ENV['URL_HOST']}"
  end

  def now
    ROTP::TOTP.new(otp_secret).now
  end

  def refresh!
    return if activated?
    super
  end

  private

  def gen_code
    self.otp_secret = ROTP::Base32.random_base32
    self.refreshed_at = Time.new
  end

  def send_notification
    return if not self.activated_changed?

    if self.activated
      MemberMailer.google_auth_activated(member.id).deliver
    else
      MemberMailer.google_auth_deactivated(member.id).deliver
    end
  end

end

EDIT: app\models\two_factor.rb:

class TwoFactor < ActiveRecord::Base
  belongs_to :member

  before_validation :gen_code, on: :create
  after_update :send_notification

  validates_presence_of :member, :otp_secret, :refreshed_at

  attr_accessor :otp

  SUBCLASS = ['app', 'sms', 'email', 'wechat']

  validates_uniqueness_of :type, scope: :member_id

  scope :activated, -> { where(activated: true) }
  scope :require_signin, -> { where(require_signin: 1) }

  class << self
    def by_type(type)
      return if not SUBCLASS.include?(type.to_s)

      klass = "two_factor/#{type}".camelize.constantize
      klass.find_or_create_by(type: klass.name)
    end

    def activated?
      activated.any?
    end

    def require_signin?
      require_signin.any?
    end
  end

  def verify?
    msg = "#{self.class.name}#verify? is not implemented."
    raise NotImplementedError.new(msg)
  end

  def expired?
    Time.now >= 30.minutes.since(refreshed_at)
  end

  def refresh!
    gen_code
    save
  end

  def active!
    update activated: true, last_verify_at: Time.now
  end

  def set_require_signin
    update require_signin: 1
  end

  def reset_require_signin
    update require_signin: nil
  end

  def deactive!
    update activated: false, require_signin: nil
  end

  private

  def gen_code
    msg = "#{self.class.name}#gen_code is not implemented."
    raise NotImplementedError.new(msg)
  end

  def send_notification
    msg = "#{self.class.name}#send_notification is not implemented."
    raise NotImplementedError.new(msg)
  end

end
lacostenycoder
  • 8,780
  • 3
  • 23
  • 38
Spero Coin
  • 41
  • 9

2 Answers2

2

What it seems you're trying to do is just to copy the value of an input field(which has been populated by other code you have) to the system clipboard. You need to use javascript to do this, if you have jquery this should work.

For your slim you need an id to target it

a.btn.btn-default id= "copy"
  i.fa.fa-clipboard

Try to add an id to the input element you want to copy from

= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true 

Now try to change this and see if works.

a.btn.btn-default data-clipboard-action='copy' data-clipboard-target='secret'
  i.fa.fa-clipboard

Also somewhere in your javascript you'll need to target the clip event with something like this:

new ClipboardJS('#secret');

See example here https://jsfiddle.net/ec3ywrzd/

Then you'll need this javascript to load in your html. But you'll need to be able to target the cipher field, in this example I'm using id="secret". I'm not sure if the OTP code you have generates it's own ID or now, so you may need to inspect your dom to figure out how to target it to add an ID. You may try adding an ID here:

= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true 

Otherwise you'll have to use other query selectors to target it. But you may not need clipboardjs at all.

Here's a basic example on jsfiddle to test it you can just add any string to the input field. You'll need to add this to a JS file which will be loaded by your view layout, i.e. application.js

$(document).ready(function() {
  $('#copy').click(function(){
    $('#secret').select();
    document.execCommand('copy');
    alert("copied!");
  })
})

You may also see answers to this question

lacostenycoder
  • 8,780
  • 3
  • 23
  • 38
  • I tried it : "= f.input_field :otp_secret, class: 'upcase', id:'secret', readonly: true span.input-group-btn a.btn.btn-default data-clipboard-action='copy' data-clipboard-target='secret' i.fa.fa-clipboard" and it didn't work. I would really like to continue using only clipboard.js ... – Spero Coin Feb 11 '20 at 14:28
  • 1
    see updated answer, you need `new ClipboardJS('.#secret);` – lacostenycoder Feb 11 '20 at 18:01
0

I managed to solve based on suggestions from our friend @lacostenycoder.

There was only a need to change even in the show.html.slim file, looking like this:

= simple_form_for @google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
  h4 = t('.step-1')
  p
    span = t('.download-app')
    span == t('.guide-link')

  h4 = t('.step-2')
  p: span = t('.scan-qr-code')

  = f.input :uri do
    = qr_tag(@google_auth.uri)

  = f.input :otp_secret do
    .input-group
      .form-control.form-control-static = @google_auth.otp_secret
      .input-group
          a.btn.btn-default href="javascript:void(0)" data-clipboard-text = @google_auth.otp_secret
            i.fa.fa-clipboard
          a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
            i.fa.fa-refresh

  h4 = t('.step-3')
  p: span = t('.enter-passcode')

  = f.input :otp

  hr.split
  = f.button :wrapped, t('.submit'), cancel: settings_path

= content_for :guide do
  ul.list-unstyled
    li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
      i.fa.fa-apple
      span = t('.ios')
    li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
      i.fa.fa-android
      span = t('.android')
Spero Coin
  • 41
  • 9