Last active
October 11, 2017 18:31
-
-
Save johno/a0526adaf84714ad3ce65f76aaa024a1 to your computer and use it in GitHub Desktop.
Login link flow with Rails, Clearance, and has_secure_token
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class LoginLink < ApplicationRecord | |
| has_secure_token | |
| belongs_to :user | |
| before_create :assign_secure_phrase_and_expires_at | |
| def expired? | |
| expires_at < Time.now | |
| end | |
| private | |
| def assign_secure_phrase_and_expires_at | |
| self.secure_phrase = generate_secure_phrase | |
| self.expires_at = 15.minutes.from_now | |
| end | |
| def generate_secure_phrase | |
| items = %w( | |
| planet moon galaxy nebula star_cluster constellation | |
| star meteorite | |
| ) | |
| [ | |
| "The #{Faker::Space.send(items.sample)}", | |
| "is #{Faker::Space.distance_measurement}", | |
| "from #{Faker::Space.send(items.sample)}" | |
| ].join(' ') | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'rails_helper' | |
| RSpec.describe LoginLink, type: :model do | |
| it 'generates a secure phrase and token' do | |
| result = FactoryGirl.create(:login_link) | |
| expect(result.token).to be_present | |
| expect(result.expires_at).to be_present | |
| expect(result.secure_phrase).to be_present | |
| end | |
| describe '#expired?' do | |
| context 'when expired' do | |
| it 'returns true' do | |
| login_link = FactoryGirl.create(:login_link) | |
| login_link.expires_at = 1.minute.ago | |
| expect(login_link).to be_expired | |
| end | |
| end | |
| context 'when not expired' do | |
| it 'returns false' do | |
| result = FactoryGirl.create(:login_link) | |
| expect(result).not_to be_expired | |
| end | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class LoginLinksController < ApplicationController | |
| def create | |
| user = User.find_by(email: login_link_params[:email]) | |
| login_link = LoginLink.new(user: user) | |
| if login_link.save | |
| # Send email here with constructed link and secure phrase | |
| render json: { | |
| loginLink: { | |
| id: login_link.id, | |
| expiresAt: login_link.expires_at, | |
| securePhrase: login_link.secure_phrase | |
| } | |
| }, status: :created | |
| else | |
| render json: { error: 'Invalid request' }, status: :bad_request | |
| end | |
| end | |
| def show | |
| login_link = LoginLink.find_by!(id: params[:id], token: params[:token]) | |
| if login_link.expired? | |
| render json: { error: 'Invalid or expired link' }, status: :bad_request | |
| else | |
| login_link.update(expires_at: Time.now) | |
| session_token = SessionToken.create!( | |
| user: login_link.user, | |
| metadata: metadata | |
| ) | |
| render json: { sessionToken: session_token }, status: :ok | |
| end | |
| end | |
| private | |
| def login_link_params | |
| params.require(:loginLink).permit(:email) | |
| end | |
| def metadata | |
| { | |
| ip: request.ip, | |
| user_agent: request.user_agent, | |
| login_link_id: params[:id] | |
| } | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'rails_helper' | |
| RSpec.describe 'LoginLinks', type: :request do | |
| describe '#create' do | |
| context 'when passed an email' do | |
| it 'returns a login link' do | |
| user = User.create(email: '[email protected]', password: :password) | |
| post '/login_links', { | |
| format: :json, | |
| loginLink: { | |
| email: user.email | |
| } | |
| } | |
| result = json['loginLink'] | |
| expect(result['token']).to be_present | |
| expect(result['expiresAt']).to be_present | |
| expect(result['securePhrase']).to be_present | |
| end | |
| end | |
| context 'when passed a non existent email' do | |
| it 'returns a 406' do | |
| post '/login_links', { | |
| format: :json, | |
| loginLink: { | |
| email: '[email protected]' | |
| } | |
| } | |
| result = last_response | |
| expect(result.status).to eq(400) | |
| end | |
| end | |
| end | |
| describe '#show' do | |
| context 'when passed a valid link' do | |
| it 'returns the token' do | |
| login_link = FactoryGirl.create(:login_link) | |
| get '/login_links', { | |
| format: :json, | |
| id: login_link.id, | |
| token: login_link.token | |
| } | |
| result = json['sessionToken']['id'] | |
| expect(login_link.user.session_token_ids).to include(result) | |
| end | |
| end | |
| context 'when passed an invalid link' do | |
| it 'returns a 400' do | |
| get '/login_links', id: :fake, format: :json | |
| result = last_response | |
| expect(result.status).to eq(400) | |
| end | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class SessionToken < ApplicationRecord | |
| has_secure_token | |
| belongs_to :user | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class User < ApplicationRecord | |
| include Clearance::User | |
| has_many :login_links | |
| has_many :session_tokens | |
| validates :email, email_format: true | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment