Giter VIP home page Giter VIP logo

sinatra-warden-example's Introduction

Sinatra Warden Example

This readme is copied from the original blog post on my site.

UPDATE 5/18/2014, Switched from Rack::Flash to Sinatra/Flash and added instructions for launching the app.

In this article I'll explain the basics of authentication and Rack middleware and in the process build a complete app with Sinatra, DataMapper and Warden.

Audience

This article is intended for people familiar with Sinatra and DataMapper who want multiple user authentication.

If you've never built a website with Sinatra I'd recommend Peepcode's excellent Meet Sinatra screencast, it is definitely worth the twelve dollars.

Storing Passwords

Passwords should never be stored in plain text. If someone were to get access to your database they'd have all of the passwords. You'd have everyone's passwords. We need to encrypt the passwords. DataMapper supports a BCryptHash property type which is great because bcrypt is pretty dang secure.

Let's get started on a User model. For the rest of this section we will be building a file named model.rb in stages. The first step is to install the gems we need:

$ gem install data_mapper
$ gem install dm-sqlite-adapter

When installing the data_mapper gem bcrypt-ruby is installed as a dependency.

Note: you may need to run the above gem commands with sudo if you are not using rvm.

Open up (or create) a file named model.rb and require the gems and set up DataMapper:

/model.rb
require 'rubygems'
require 'data_mapper'
require 'dm-sqlite-adapter'
require 'bcrypt'

DataMapper.setup(:default, "sqlite://#{Dir.pwd}/db.sqlite")

Now let's create a User model. In addition to including DataMapper::Resource we will include the BCrypt class (the gem is named 'bcrypt-ruby', it is required as 'bcrypt' and the class is named BCrypt).

/model.rb (cont.)
#...

class User
  include DataMapper::Resource

  property :id, Serial, :key => true
  property :username, String, :length => 3..50
  property :password, BCryptHash
end

DataMapper.finalize
DataMapper.auto_upgrade!

# end of model.rb

Let's test this code.

$ irb
> require './model'
> @user = User.new(:username => "admin", :password => "test")
> @user.save
> @user.password
# => "$2a$10$lKgran7g.1rSYY0M6d0V9.uLInljHgYmrr68LAj86rllmApBSqu0S"
> @user.password == 'test'
# => true
> @user.password
# => "$2a$10$lKgran7g.1rSYY0M6d0V9.uLInljHgYmrr68LAj86rllmApBSqu0S"
> exit

Excellent. We have a User model that stores passwords in an encrypted way.

If you'd like to see another take on using bcrypt, Github user namelessjon has a more complex example with some discussion here.

Warden, a Library for Authentication and User Sessions

Warden is an excellent gem for authentication with Sinatra. I've found that the documentation for Warden is lacking which is why I'm writing this. If you want to know the why of Warden read this.

You may have seen that there is a gem called sinatra_warden. Why am I not using that? The sinatra_warden gem chooses the routes for logging in and logging out for you and that logic is buried in the gem. I like for all of the routes in my Sinatra apps to be visible at a glance and not squirreled away.

But ok, on to Warden.

After struggling a lot with figuring out how to set up Warden I found this post by Mike Ebert extremely helpful.

Warden is middleware for Rack. Sinatra runs on Rack. Rack is an adapter to let Sinatra run on many different web servers. Warden lives between Rack and Sinatra.

I use bundler with Sinatra, this is the Gemfile for this example app. Before You'll need to create that Gemfile in your directory and run the following in Terminal:

$ bundle install

We're using sinatra-flash to show alerts on pages, the first chunk of code will load our gems and create a new Sinatra app and register session support and the flash messages:

/app.rb
require 'bundler'
Bundler.require

# load the Database and User model
require './model'

class SinatraWardenExample < Sinatra::Base
  enable :sessions
  register Sinatra::Flash

#...

Now in the Warden setup. Most of the lines need to be explained so I'll mark up the code with comments. This block tells Warden how to set up, using some code specific to this example, if your user model is named User and has a key of id this block should be the same for you, otherwise, replace where you see User with your model's class name.

/app.rb (cont)
  use Warden::Manager do |config|
    # Tell Warden how to save our User info into a session.
    # Sessions can only take strings, not Ruby code, we'll store
    # the User's `id`
    config.serialize_into_session{|user| user.id }
    # Now tell Warden how to take what we've stored in the session
    # and get a User from that information.
    config.serialize_from_session{|id| User.get(id) }

    config.scope_defaults :default,
      # "strategies" is an array of named methods with which to
      # attempt authentication. We have to define this later.
      strategies: [:password],
      # The action is a route to send the user to when
      # warden.authenticate! returns a false answer. We'll show
      # this route below.
      action: 'auth/unauthenticated'
    # When a user tries to log in and cannot, this specifies the
    # app to send the user to.
    config.failure_app = self
  end

  Warden::Manager.before_failure do |env,opts|
    # Because authentication failure can happen on any request but
    # we handle it only under "post '/auth/unauthenticated'", we need
    # to change request to POST
    env['REQUEST_METHOD'] = 'POST'
    # And we need to do the following to work with  Rack::MethodOverride
    env.each do |key, value|
      env[key]['_method'] = 'post' if key == 'rack.request.form_hash'
    end
  end

The last part of setting up Warden is to write the code for the :password strategy we called above. In the following block, they keys of params which I am using are based on the login form I made.

/app.rb (cont)
  Warden::Strategies.add(:password) do
    def valid?
      params['user'] && params['user']['username'] && params['user']['password']
    end

    def authenticate!
      user = User.first(username: params['user']['username'])

      if user.nil?
        throw(:warden, message: "The username you entered does not exist.")
      elsif user.authenticate(params['user']['password'])
        success!(user)
      else
        throw(:warden, message: "The username and password combination ")
      end
    end
  end

Hold on a minute. I called an authenticate method on user. We need to create such a method in our User class that accepts an attempted password. Back in model.rb we'll add the following:

/model.rb (reopened)
class User
  #...

  def authenticate(attempted_password)
    if self.password == attempted_password
      true
    else
      false
    end
  end
end

Time to define a few routes to handle logging in, logging out and a protected page.

/app.rb (cont)
  get '/' do
    erb :index
  end

  get '/auth/login' do
    erb :login
  end

  post '/auth/login' do
    env['warden'].authenticate!

    flash[:success] = "Successfully logged in"

    if session[:return_to].nil?
      redirect '/'
    else
      redirect session[:return_to]
    end
  end

  get '/auth/logout' do
    env['warden'].raw_session.inspect
    env['warden'].logout
    flash[:success] = 'Successfully logged out'
    redirect '/'
  end

  post '/auth/unauthenticated' do
    session[:return_to] = env['warden.options'][:attempted_path] if session[:return_to].nil?

    # Set the error and use a fallback if the message is not defined
    flash[:error] = env['warden.options'][:message] || "You must log in"
    redirect '/auth/login'
  end

  get '/protected' do
    env['warden'].authenticate!

    erb :protected
  end
end

Starting The App

As @Celandir has pointed out, this app uses the Sinatra modular-style app. To run a modular app we use a file named config.ru (the "ru" stands for rackup).

There are two ways to run this app.

rackup

When you've ran bundle install you'll get a program named 'rackup' which will run the app on port 9292 by default. You need to run "rackup" with the config.ru file, as such:

$ rackup config.ru
# [2014-05-18 12:11:27] INFO  WEBrick 1.3.1
# [2014-05-18 12:11:27] INFO  ruby 2.0.0 (2014-02-24) [x86_64-darwin13.1.0]
# [2014-05-18 12:11:27] INFO  WEBrick::HTTPServer#start: pid=72027 port=9292

With that running in Terminal visit http://localhost:9292 to see the app.

shotgun

There is a ruby gem called shotgun which is very useful in development because it will pick up changes to your ruby files. So you won't need to stop and restart the server every time you change a file. To use shotgun with our config.ru file, you need to tell shotgun which file to use, like so:

$ shotgun config.ru
# == Shotgun/Thin on http://127.0.0.1:9393/
# >> Thin web server (v1.4.1 codename Chromeo)
# >> Maximum connections set to 1024
# >> Listening on 127.0.0.1:9393, CTRL+C to stop

Shotgun runs apps on a different port than rackup, if you are using shotgun visit the app at http://localhost:9393.

shotgun and flash messages

The flash plugin makes use of sessions to store messages across routes. The sessions are stored with a "secret" generated each time the server starts. shotgun works by restarting the server at every request, which means your flash messages will be lost.

To enable flash messages with shotgun, you must specifically set :session_secret using the following:

class SinatraWardenExample < Sinatra::Base
  enable :sessions
  register Sinatra::Flash
  set :session_secret, "supersecret"
#...

Always be careful with storing secret keys in your source code. In fact, it's advisable to not do so, and instead use an ENV variable as such:

set :session_secret, ENV['SESSION_SECRET']

I figured this out by reading this very helpful StackOverflow answer.

sinatra-warden-example's People

Contributors

awesomelionel avatar jkvalk avatar sklise avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sinatra-warden-example's Issues

Misleading example for user authenticate

First off, this is a very useful project - thank you.

The small nit I want to raise is that you propose a user authentication method that implies storage of a plaintext password: if self.password == attempted_password

I realize the point of this example is to streamline the description. So perhaps just put a comment on this line # DO NOT use this in a real application - always encrypt your passwords in a production app (if you use ActiveRecord see "has_secure_password" feature).

Flash messages not appearing

Hi, I'm still working on getting this app running. None of the flash messages seem to appear. Through debugging, I proved that the proper routes are called, the Warden.authenticate! method is called, and the user is successfully authenticated. However, there is no flash message to say that they were authenticated successfully. Also, I cannot access the protected page even if I login with the correct password.

When I login with an incorrect password, there is no failure flash message. As above, I verified that the Warden authenticate method is called, and the unsuccessful authentication branch is executed.

Appreciate any help you can provide; thx. FYI I am trying to run the source you provided from Github.

Minor request: add instruction to use rackup

Hi, thx for posting this example. I am still new with using Sinatra, so I mostly blame this issue on myself, however I was unsuccessful with your application when I launched it via shotgun. However, after reading the Sinatra readme, I learned that I should be using rackup since this is a modular application which extends the Sinatra base class. Once I launched with rackup, I was successful.

So my minor request is to add an instruction/reminder that users should use rackup rather than shotgun to execute this code.

Greetings from the future (3 hiccups for ppl using this example)

There are three small hiccups (at least) in this code that made it not work in my application:

  1. Generally in sinatra, you can use params[:symbol]. The Warden strategy, however, requires params["string"].
  2. flashing did not work for some reason until I changed env['warden'].message to env['warden.options'][:message].
  3. DataMapper yells at me when I include BCrypt. Removing that worked fine, and the password is still hashed.

These issues came up with:

  • bcrypt-ruby 3.1.5 (vs. 3.0.1 in this example)
  • dm-core 1.2.0 (as in this example)
  • rack 1.6.4 (vs. 1.5.2)
  • sinatra 1.4.6 (vs. 1.3.4)
  • sinatra-flash 0.3.0 (as in this example)
  • warden 1.2.3 (as in this example)

I imagine versions may have been causing some of the problems, but I'm not certain. Either way, than you very much for this example!

Redirecting in POST route.

Great repo and tutorial.
I have an issue with redirection in POST route. User is able to log in but in route which handles user image upload, once I am done with uploading file and updating my db I want to redirect my user to my welcome page. That "throws" me out and asks for log in again. Here is code snippet:

post '/upload' do
    puts session.inspect()
    path = 'public/user_images/' + params['file'][:filename]
    File.open(path, 'w') do |f|
      f.write(params['file'][:tempfile].read)
    end
    u_db =  @@user[0]
    u_db.avatar = params['file'][:filename]
    u_db.save
   redirect '/welcome'
end

Here is server output:
{"warden.user.default.key"=>1, "flash"=>{:success=>nil}, "session_id"=>"742147cf8ae515cfb08e5c0ded8f3ec322f8de8df1035f3332083bbd5bf9299f", "tracking"=>{"HTTP_USER_AGENT"=>"cdddb802ce3021c57f126425b7b73bcf40b1587d", "HTTP_ACCEPT_ENCODING"=>"ed2b3ca90a4e723402367a1d17c8b28392842398", "HTTP_ACCEPT_LANGUAGE"=>"13756b51e9becdd4494e477d4e752f5bdb583f8e"}}
127.0.0.1 - - [03/Nov/2014 20:11:36] "GET /welcome HTTP/1.1" 200 810 0.0262
127.0.0.1 - - [03/Nov/2014 20:11:36] "GET /public/stylesheets/style.css HTTP/1.1" 404 464 0.0021
127.0.0.1 - - [03/Nov/2014 20:11:40] "GET /upload HTTP/1.1" 200 929 0.0087
127.0.0.1 - - [03/Nov/2014 20:11:40] "GET /public/stylesheets/style.css HTTP/1.1" 404 464 0.0026
{}
127.0.0.1 - - [03/Nov/2014 20:11:46] "POST /upload HTTP/1.1" 303 - 0.1963
{"session_id"=>"742147cf8ae515cfb08e5c0ded8f3ec322f8de8df1035f3332083bbd5bf9299f", "tracking"=>{"HTTP_USER_AGENT"=>"cdddb802ce3021c57f126425b7b73bcf40b1587d", "HTTP_ACCEPT_ENCODING"=>"ed2b3ca90a4e723402367a1d17c8b28392842398", "HTTP_ACCEPT_LANGUAGE"=>"13756b51e9becdd4494e477d4e752f5bdb583f8e"}}
127.0.0.1 - - [03/Nov/2014 20:11:46] "POST /welcome HTTP/1.1" 303 - 0.0035
127.0.0.1 - - [03/Nov/2014 20:11:46] "GET /auth/login HTTP/1.1" 200 776 0.0041

I think I narrowed the problem down to warden. I am printing session.inspect and it can be seen that the warden part is lost after POST call. session_id is the same but part relating to warden is gone and that is why redirection to '/welcome' is rerouted to '/auth/login'

Any ideas what is wrong?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.