-
Notifications
You must be signed in to change notification settings - Fork 2
Example: Adding authentication
In this entry we'll explain how to use warden and sequel_secure_password gems to add authentication to your app.
We'll start with a vanilla yogurt template and create a login page at /user_sessions/new
. If the user is already authenticated or successfully authenticates he can visit the /communities
routes, otherwise he gets a 403 status code and markup requesting login.
If you just wanna grab the code, check out the warden-auth
branch: https://github.com/badosu/Yogurt/tree/warden-auth or commit: 977271e
First of all, let's create an User
model with email and password:
# db/migrations/2_create_users.rb
Sequel.migration do
change do
create_table(:users) do
primary_key :id
column :email, String, null: false
column :password_digest, String
column :created_at, DateTime, null: false
column :updated_at, DateTime
end
end
end
We use the password_digest
as it is the convention on sequel_secure_password
for the column name, but that can be configured.
Run the migration: bin/bs bundle exec rake db:migrate
.
With a simple validation for email presence:
# models/user.rb
class User < Sequel::Model
def validate
super
errors.add(:email, 'must be present') if !email || email.empty?
end
end
Add sequel_secure_password
to your Gemfile
and run the usual bundling process.
This is a dead-simple gem and if you don't want to use it, just make sure that your model responds to #authenticate(password)
with itself if it passes and nil
if not. See the source code.
As this is a model plugin, and should be available to all models, append require 'secure_sequel_password'
to the ./models.rb
file header if you're not using Bundler.
Now you can simply use the Sequel convention for including plugins to a class, in this case for our User
class:
diff --git a/models/user.rb b/models/user.rb
index ea8787d..8b8ce88 100644
--- a/models/user.rb
+++ b/models/user.rb
@@ -1,4 +1,6 @@
class User < Sequel::Model
+ plugin :secure_password
+
def validate
super
errors.add(:email, 'must be present') if !email || email.empty?
This .plugin
call can accept configuration that is described on sequel_secure_password
README. In particular we'll use the default one, which includes validations for the password presence and confirmation.
This is what's available for us now:
user = User.new
user.password = "foo"
user.password_confirmation = "bar"
user.valid? # => false
user.password_confirmation = "foo"
user.valid? # => true
user.authenticate("foo") # => user
user.authenticate("bar") # => nil
Let's seed the database (and skip validations):
User.new(email: '[email protected]',
password: 'youropinionman').
save(validate: false)
Now we have implemented password validation logic into our User
model, it was pretty straightforward and you can see what's happening under the hood here.
It's that simple!
Let's create a sign-in page, for this we'll use a nice Bootstrap example.
We'll use the concept of a user session as a resource to be created to abstract the signin process, as it's done with Devise.
Copy the html of that example and put it in on the views/user_sessions/new.html.erb
file.
Put the signin.css
file on assets/css/signin.css
and add it to the yogurt.min.css
bundle:
diff --git a/yogurt.rb b/yogurt.rb
index 2dbf94c..6b978cf 100644
--- a/yogurt.rb
+++ b/yogurt.rb
@@ -12,7 +12,7 @@ class Yogurt < Roda
plugin :multi_route
plugin :assets, group_subdirs: false,
css: { home: %w[lib/bootstrap.css jumbotron.css],
- yogurt: %w[lib/bootstrap.css yogurt.css] },
+ yogurt: %w[lib/bootstrap.css yogurt.css signin.css] },
js: { yogurt: %w[lib/jquery-2.1.3.js lib/bootstrap.js] }
plugin(:not_found) { view '/http_404' }
Adjust style.css
and load it on user_sessions/new.html.erb
by replacing the stylesheet links with <%= assets([:css, :yogurt]) %>
.
Also add <%= assets([:js, :yogurt]) %>
to load the relevant javascript.
Create the following file:
# routes/user_sessions.rb
class Yogurt
route 'user_sessions' do |r|
r.get 'new' do
render '/user_sessions/new'
end
end
end
Visit /user_sessions/new
, that's it.
We'll post the form at the sign in page to the user sessions collection route and redirect the user to the communities route if it's succesful, or re-render the page otherwise.
Change the markup on views/user_sessions/new.html.erb
accordingly, the form part should look like this:
<!-- ... -->
<form class="form-signin" action='/user_sessions' method='POST'>
<h2 class="form-signin-heading">Please sign in</h2>
<label for="email" class="sr-only">Email address</label>
<input type="email" name='email' class="form-control" placeholder="Email address" required autofocus>
<label for="password" class="sr-only">Password</label>
<input type="password" name="password" class="form-control" placeholder="Password" required>
<!-- ... -->
And the authentication logic:
# routes/user_sessions.rb
# ...
route 'user_sessions' do |r|
r.is do
r.post do
user = User.first(email: r['email'])
if user && user.authenticate(r['password'])
r.redirect '/communities'
else
render '/user_sessions/new'
end
end
end
# ...
Verify that our seed data passes and it works as intended, great!
There are lots of stuff to do yet, for example: we do not really create a user session nor persist it. Also we don't secure that the /communities
pages check authentication. Hopefully, we'll be able to address these issues with Warden.
First include warden
to your Gemfile and configure it:
# config/warden.rb
require 'warden'
Warden::Manager.serialize_into_session{|user| user.id }
Warden::Manager.serialize_from_session{|id| User[id] }
Warden::Strategies.add(:password) do
def valid?
params["email"] || params["password"]
end
def authenticate!
user = User.first(email: params["email"])
if user && user.authenticate(params["password"])
success! user
else
fail! "Could not log in"
end
end
end
Configure your application to use warden as a middleware:
# ./yogurt.rb
#...
require './config/warden'
class Yogurt < Roda
#...
use Warden::Manager do |manager|
manager.scope_defaults :default,
strategies: [:password],
action: 'user_sessions/unauthenticated'
manager.failure_app = self
end
Finally, let's update our authentication routes:
# routes/user_sessions.rb
class Yogurt
route 'user_sessions' do |r|
r.is do
r.post do
env['warden'].authenticate!
r.redirect session[:return_to] || '/communities'
end
end
r.get 'new' do
render '/user_sessions/new'
end
r.is 'unauthenticated' do
session[:return_to] = env['warden.options'][:attempted_path]
response.status = 403
render '/user_sessions/new'
end
end
end
Add the authentication check for all the communities routes:
diff --git a/routes/communities.rb b/routes/communities.rb
index f9a95fa..c056d4e 100644
--- a/routes/communities.rb
+++ b/routes/communities.rb
@@ -2,6 +2,8 @@ class Yogurt
route 'communities' do |r|
set_view_subdir 'communities'
+ env['warden'].authenticate!
+
r.is do
r.get do
@communities = Community.order(Sequel.desc(:created_at)).all
Verify it works as expected.
We'll send DELETE
to /user_sessions
to logout the application:
# routes/user_sessions.rb
class Yogurt
route 'user_sessions' do |r|
r.is do
r.post do
env['warden'].authenticate!
r.redirect session[:return_to] || '/communities'
end
r.delete do
env['warden'].logout
r.redirect '/user_sessions/new'
end
end
#...
Let's add a logout button to the layout header:
# views/layout.html.erb
<!-- ... -->
<li role="presentation"><a href="#">Contact</a></li>
<% if env['warden'].authenticated? %>
<li role="presentation">
<form class='inline' action='/user_sessions' method='POST'>
<input type='hidden' name='_method' value='DELETE'>
<button type="submit" class="btn btn-warning">Logout</button>
</form>
</li>
<% end %>
</ul>
<!-- ... -->
See commit: 8ef447e