Authentication protocol for use in your routing and model context
n. A solid piece of metal code used to protect your application.
Shield::Model
is a very basic protocol for doing authentication
against your model. It doesn't assume a lot, apart from the following:
User.fetch
which receives the login string.crypted_password
which is able to store
up to 192 characters.And that's it.
In order to implement the model protocol, you start by
including Shield::Model
.
class User < Struct.new(:email, :crypted_password)
include Shield::Model
def self.fetch(email)
user = new(email)
user.password = "pass1234"
return user
end
end
By including Shield::Model
, you get all the general methods needed
in order to do authentication.
User.authenticate
which receives the login string and
password as the two parameters.User#password=
which automatically converts the clear text
password into a hashed form and assigns it into #crypted_password
.u = User.new("foo@bar.com")
# A password accessor has been added which manages `crypted_password`.
u.password = "pass1234"
Shield::Password.check("pass1234", u.crypted_password)
# => true
# Since we've hard coded all passwords to pass1234
# we're able to authenticate properly.
nil == User.authenticate("foo@bar.com", "pass1234")
# => false
# If we try a different password on the other hand,
# we get `nil`.
nil == User.authenticate("foo@bar.com", "wrong")
# => true
Shield includes tests for ohm and sequel and makes sure that each release works with the latest respective versions.
Take a look at test/ohm.rb and test/sequel.rb to learn more.
If your requirements dictate that you need to be able to support logging
in using either username or email, then you can simply extend User.fetch
a bit by doing:
# in Sequel
class User < Sequel::Model
def self.fetch(identifier)
filter(email: identifier).first || filter(username: identifier).first
end
end
# in Ohm
class User < Ohm::Model
attribute :email
attribute :username
unique :email
unique :username
def self.fetch(identifier)
with(:email, identifier) || with(:username, identifier)
end
end
If you want to allow case-insensitive logins for some reason, you can simply normalize the values to their lowercase form.
As the name suggests, Shield::Helpers
is out there to aid you a bit,
but this time it aids you in the context of your Rack application.
Shield::Helpers
assumes only the following:
env
method which returns the environment hash as
was passed in Rack.Note: As of this writing, Sinatra, Cuba & Rails adhere to having an env
method in the handler / controller context. Shield also ships with tests for
both Cuba and Sinatra.
require "sinatra"
# Satisfies assumption number 1 above.
use Rack::Session::Cookie
# Mixes `Shield::Helpers` into your routes context.
helpers Shield::Helpers
get "/private" do
error(401) unless authenticated(User)
"Private"
end
get "/login" do
erb :login
end
post "/login" do
if login(User, params[:login], params[:password], params[:remember_me])
redirect(params[:return] || "/")
else
redirect "/login"
end
end
get "/logout" do
logout(User)
redirect "/"
end
__END__
@@ login
<h1>Login</h1>
<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'>
Note for the reader: The redirect to params[:return]
in the example
is vulnerable to URL hijacking. You can whitelist redirectable urls, or
simply make sure the URL matches the pattern /\A[\/a-z0-9\-]+\z/i
.
If you have a keen eye you might have noticed that instead of redirecting
away to the login URL in the example above, we instead chose to do a
401 Unauthorized
. In strict HTTP Status code terms, this is the proper
approach. The redirection is simply the user experience pattern that has
emerged in web applications.
But don't despair! If you want to do redirects simply add
Shield::Middleware
to your middleware stack like so:
# taken from example above
use Shield::Middleware, "/login"
use Rack::Session::Cookie
# rest of code follows here
# ...
Now when your application responds with a 401
, Shield::Middleware
will be responsible for doing the redirect to /login
.
If you try and do a curl --head http://localhost:4567/private
with
Shield::Middleware
, you'll get a response similar to the following:
HTTP/1.1 302 Found
Location: http://localhost:4567/login?return=%2Fprivate
Content-Type: text/html
Notice that it specifies /private
as the return URL.
For people interested in using Cuba, Ohm, Shield and Bootstrap we've created a starting point that includes Login, Signup and Forgot Password functionality.
Head on over to the cuba-app repository if you want to know more.