Create a gist now

Instantly share code, notes, and snippets.

@bemurphy /app.rb Secret
Last active Dec 18, 2015

require "ohm"
require "redis"
require "securerandom"
require "shield"
require "sinatra"
Ohm.connect db: 14
class User < Ohm::Model
include Shield::Model
attribute :email
attribute :crypted_password
index :email
def self.fetch(email)
User.find(email: email).first
end
end
User.find(email: 'john@example.com').first || User.create(email: 'john@example.com', password: 'secret')
class ChallengeAuthentication
EXPIRE_TIME = 300
attr_reader :user
def initialize(user)
@user = user
end
def redis
Ohm.redis
end
def push
challenge = generate_challenge
redis.setex key, EXPIRE_TIME, challenge
deliver_challenge(challenge)
end
def check(challenge)
return false if user.nil? || challenge.to_s.empty?
# Should be a secure compare to prevent timing attacks
redis.get(key) == challenge
end
def check!(challenge)
!! ( check(challenge) && redis.del(key) )
end
def key
[user.class.name, user.id, 'challenge'].join(':')
end
# Returns a 6 digit challenge phrase
def generate_challenge
(SecureRandom.random_number * 1_000_000).to_i
end
def deliver_challenge(challenge)
# send an out of band challenge like SMS or Pushover here
end
end
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
helpers do
include Shield::Helpers
alias_method :initially_authenticated, :authenticated
def authenticated(model)
if user = initially_authenticated(model)
user.id.to_s == session["#{model}_secondary_auth"].to_s && user
end
end
def challenge_authentication(model)
ChallengeAuthentication.new(initially_authenticated(model))
end
def send_challenge(model)
challenge_authentication(model).push
end
def challenge_accepted(model, challenge)
if challenge_authentication(model).check!(challenge)
user = initially_authenticated(model)
session["#{model}_secondary_auth"] = user.id.to_s
end
end
end
error 401 do
redirect '/login'
end
# Handle initial password login
get '/login' do
erb :login
end
post '/login' do
if login(User, params[:login], params[:password])
remember(initially_authenticated(User)) if params[:remember_me]
send_challenge(User)
redirect '/login_verification'
else
redirect '/login'
end
end
# Handle second factor authentication if password auth succeeds
get '/login_verification' do
error(401) unless initially_authenticated(User)
erb :login_verification
end
post '/login_verification' do
error(401) unless initially_authenticated(User)
if challenge_accepted(User, params[:challenge])
redirect '/'
else
redirect '/login_verification'
end
end
get '/' do
error(401) unless authenticated(User)
"You're in!"
end
__END__
@@login
<form action='/login' method='post'>
<input type='text' name='login' placeholder='Email'>
<input type='password' name='password' placeholder='Password'>
<input type='submit' name='proceed' value='Login'>
</form>
@@login_verification
<form action='/login_verification' method='post'>
<input type='text' name='challenge' placeholder='Challenge'>
<input type='submit' name='proceed' value='Login'>
</form>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment