Integrating Basic Authorization in Rails
In this guide we'll step through how to integrate basic authorization in a Ruby on Rails application.
Guide Tasks
  • Read Tutorial

So our Post feature is coming along, however we have an issue, right now all posts can be edited by anyone visiting the site. This means that if you create a post anyone can come along and change it, if you don't fix this bug...

medium

We also have another issue, un-registered users can create posts, this is really bad because our post creation workflow requires that a post belongs_to a user, without that value you're app is going to have errors all over the place. So before we get started let's list what needs to be done:

  1. A user should be required to be logged in to access the new form view

  2. A user should be required to be logged in to access the edit view

  3. A user should only be able to edit a post that they created

Ok, I feel better now that we know what we need to do, let's start off with the new form authorization and create a spec to test this behavior. Inside of the creation block in the features/post_spec file add in the following expectation:

# spec/features/post_spec.rb

    it 'only lets signed in users view the new form' do
      logout(:user)
      visit new_post_path
      expect(current_path).to eq(new_user_session_path)
    end

Agh, more test methods! Before running the tests let's run through what this test is doing step by step:

  1. Using the Warden logout method it's killing the user session, essentially mocking what would happen if the user signed out (remember that our before block signed the user in, so we need to call this to mimc what would happen if the user wasn't signed in at all.

  2. Then we're trying to re-visit the posts/new path (we have to do this because to ensure that we're working with a user who is not signed in)

  3. Then we're using the current_path method from Capybara to check to see what URL path the mocked ghost user lands on

Running rspec will give us the following failure:

Failures:

  1) post creation only lets registered users view the new form
     Failure/Error: expect(current_path).to eq(new_user_session_path)

       expected: "/users/sign_in"
            got: "/posts/new"

       (compared using ==)
     # ./spec/features/post_spec.rb:80:in `block (3 levels) in <top (required)>'

Finished in 1.38 seconds (files took 2.53 seconds to load)
34 examples, 1 failure, 3 pending

Failed examples:

rspec ./spec/features/post_spec.rb:77 # post creation only lets registered users view the new form

That means that it thought they were going to be sent to users/sign_in but still went to the posts/new path. Now we could do something ugly, like checking on the view if the user is signed in and then use JavaScript to redirect them or something horrible like that. But there's another way, Devise ships with a very handy method called authenticate_user! that we can place in the controller action we want to protect and the method checks to see if a user is signed in and if not it redirects them to the login path, let's implement that:

# app/controllers/topics/posts_controller.rb

  def new
    authenticate_user!
  end

That's it! Now run rspec again and see that the test is passing, I always love it when a single method call implements a full feature. Now we have two final features for this lesson:

  1. A user should be required to be logged in to access the edit view

  2. A user should only be able to edit a post that they created

If you assumed that we can take care of #2 the same way we did with the new action you'd be 100% right, let's implement the spec first though (make sure to place this spec inside of the editing block:

# spec/features/post_spec.rb

    # Update the before block so it creates a @post instance variable so we can use it in the new spec
    before do
      user = FactoryGirl.create(:user)
      second_user = FactoryGirl.create(:second_user)
      login_as(user, :scope => :user)

      @post = Post.create(title: "starter title", content: "starter content", topic_id: @topic.id, user_id: user.id)

      visit edit_topic_post_path(topic_id: @topic.id, id: @post.id)
    end

    # Now we can implement the spec
    it 'does not allow a user to access the edit page if they are not signed in' do
      logout(:user)
      visit edit_topic_post_path(topic_id: @topic.id, id: @post.id)
      expect(current_path).to eq(new_user_session_path)
    end

This expectation will look very similar to the new spec, running the specs will give us the same type of failure, let's integrate the authenticate_user! method inside of the edit action in the controller:

# app/controllers/topics/posts_controller.rb

  def edit
    authenticate_user!
  end

This gets all of the specs passing, let's perform a small refactor, we're going to want the user to be logged in for every action except the index and show action, so let's have the authenticate_user! method run as a before filter in this controller:

# app/controllers/topics/posts_controller.rb

before_action :authenticate_user!, except: [:index, :show]

def new
end

def edit
end

Running the specs now will show that everything is still passing, let's see what this looks like in the browser by navigating to one of the show pages in incognito mode (to ensure we don't have a user session). Going to http://localhost:3000/topics/my-title-99/posts/2 works, so that's good, it's not blocking regular site visitors, if I click on the Edit Post button it redirects me to the sign in page and let's us know that we need to be signed in to access that page, cool, so everything is working.

large

Let's fix one thing, I don't like that all users see the Edit Post button, that should only be shown to the post author. We can fix this by updating the view with the following code:

<!-- app/views/topics/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p><%= @post.topic.title %></p>
<p><%= @post.user.full_name %></p>

<% if current_user && @post.user_id == current_user.id %>
  <%= link_to "Edit Post", edit_topic_post_path(topic_id: @topic.id, id: @post.id) %>
<% end %>

Now if you go back in the browser to the show page and click refresh you'll see the button disappears, pretty cool, right?

large

The Devise gem has some great built in methods that let us implement basic authorization very efficiently. Now let's implement the final expectation:

# spec/features/post_spec.rb

    it 'does not allow a user to edit a post they did not create' do
      logout(:user)
      login_as(@second_user, :scope => :user)
      visit edit_topic_post_path(topic_id: @topic.id, id: @post.id)
      expect(current_path).to eq(topic_post_path(topic_id: @topic.id, id: @post.id))
    end

This test is pretty similar to the other specs:

  1. It first logs out the initial user (who is also the @post creator)

  2. Logs in another user (make sure to update the before action so that the @second_user = FactoryGirl.create(:second_user) is an instance variable instead of a local variable

  3. Tries to visit the edit path for @post

  4. Expects to see the show path

Running the spec will complain that the mocked user is still able to reach the edit page, let's update this in the controller:

# app/controllers/topics/posts_controller.rb

  def edit
    if @post.user_id != current_user.id
      redirect_to topic_post_path(topic_id: @post.topic_id, id: @post), notice: 'Your are not authorized to edit this post.'
    end
  end

This implementation code checks to see if the user trying to access the page is the same user who created the post and if not it redirect the user back to the show page. This ensures that a savvy user can't append /edit at the end of the URL to change the post.

Nice work, you now know how to implement basic authorization into a Rails application. The methods we've covered are good for basic applications, however if you need an advanced permission you'll need a more comprehensive authentication implementation. In a future lesson we'll integrate the pundit gem to give our application a scalable permission structure.

The Post feature is where I want it to be right now, in the next lesson we're going to walk through how to create a pull request and merge the branch into the master branch using the GitHub web tools (as promised). For right now simply push the latest feature up to the remote branch like we have been doing:

git add .
git ci -m 'Implemented basic authorization system  for posts'
git push

Resources