Introduce ExpiringToken, a base for future authentication
parent
e06e72676e
commit
c4a4961553
@ -0,0 +1,28 @@
|
||||
class ExpiringToken < ActiveRecord::Base
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :user, :token, :expires_at, presence: true
|
||||
|
||||
scope :active, -> { where(used_at: nil).where('expires_at > ?', Time.now) }
|
||||
|
||||
def self.create_for_user(user)
|
||||
create!(
|
||||
user: user,
|
||||
token: SecureRandom.urlsafe_base64,
|
||||
expires_at: 15.minutes.from_now
|
||||
)
|
||||
end
|
||||
|
||||
def self.active_for_token(token)
|
||||
active.where(token: token).first
|
||||
end
|
||||
|
||||
def use!
|
||||
raise "token #{token} already used at #{used_at}" if used_at
|
||||
|
||||
self.used_at = Time.now
|
||||
save!
|
||||
end
|
||||
|
||||
end
|
@ -0,0 +1,13 @@
|
||||
class CreateAuthCodes < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :auth_codes do |t|
|
||||
t.references :user, index: true, null: false
|
||||
t.string :code, null: false
|
||||
t.datetime :expires_at, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :auth_codes, [:code, :expires_at]
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class AddUsedAtToAuthCodes < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :auth_codes, :used_at, :datetime
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class RemoveUpdatedAtFromAuthCodes < ActiveRecord::Migration
|
||||
def change
|
||||
remove_column :auth_codes, :updated_at
|
||||
end
|
||||
end
|
@ -0,0 +1,6 @@
|
||||
class UpdateAuthCodesIndex < ActiveRecord::Migration
|
||||
def change
|
||||
remove_index :auth_codes, name: "index_auth_codes_on_code_and_expires_at"
|
||||
add_index :auth_codes, [:used_at, :expires_at, :code]
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class RenameAuthCodesToTemporaryTokens < ActiveRecord::Migration
|
||||
def change
|
||||
rename_table :auth_codes, :temporary_tokens
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class RenameTemporaryTokensCodeToToken < ActiveRecord::Migration
|
||||
def change
|
||||
rename_column :temporary_tokens, :code, :token
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class RenameTemporaryTokensToExpiringTokens < ActiveRecord::Migration
|
||||
def change
|
||||
rename_table :temporary_tokens, :expiring_tokens
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# Read about factories at http://github.com/thoughtbot/factory_girl
|
||||
|
||||
FactoryGirl.define do
|
||||
factory :expiring_token do
|
||||
association :user
|
||||
sequence(:token) { |n| "token-#{n}" }
|
||||
expires_at { 10.minutes.from_now }
|
||||
end
|
||||
|
||||
factory :used_expiring_token, parent: :expiring_token do
|
||||
used_at { 1.minute.ago }
|
||||
end
|
||||
|
||||
factory :expired_expiring_token, parent: :expiring_token do
|
||||
expires_at { 1.minute.ago }
|
||||
end
|
||||
end
|
@ -0,0 +1,47 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ExpiringToken, :type => :model do
|
||||
|
||||
it { should validate_presence_of(:user) }
|
||||
it { should validate_presence_of(:token) }
|
||||
it { should validate_presence_of(:expires_at) }
|
||||
|
||||
describe '.create_for_user' do
|
||||
it 'creates expiring token with generated token and expiration time in the future' do
|
||||
user = create(:user)
|
||||
|
||||
expiring_token = ExpiringToken.create_for_user(user)
|
||||
|
||||
expect(expiring_token.user).to eq(user)
|
||||
expect(expiring_token.token.size).to eq(22)
|
||||
expect(expiring_token.expires_at).to be > Time.now
|
||||
end
|
||||
end
|
||||
|
||||
describe '.active_for_token' do
|
||||
it 'returns not used and not expired expiring token matching given token' do
|
||||
used_expiring_token = create(:used_expiring_token)
|
||||
expired_expiring_token = create(:expired_expiring_token)
|
||||
good_expiring_token = create(:expiring_token)
|
||||
|
||||
expect(ExpiringToken.active_for_token(used_expiring_token.token)).to be(nil)
|
||||
expect(ExpiringToken.active_for_token(expired_expiring_token.token)).to be(nil)
|
||||
expect(ExpiringToken.active_for_token(good_expiring_token.token)).to eq(good_expiring_token)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#use!' do
|
||||
it 'sets used_at to the current time and saves the record' do
|
||||
expiring_token = create(:expiring_token)
|
||||
now = Time.now
|
||||
|
||||
Timecop.freeze(now) do
|
||||
expiring_token.use!
|
||||
end
|
||||
|
||||
expect(expiring_token.used_at).to eq(now)
|
||||
expect(expiring_token).to_not be_changed
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue