-
-
Save bemurphy/831e6c3cf4d40060ed49 to your computer and use it in GitHub Desktop.
This file contains 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 "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