Authentication with Sorcery, RSpec, and Rails 7: Building a simple Rails CMS - Part 1
Tags: ruby, rails, rspec, authentication
Published: 2022-01-29 21:49:05

_Cover photo by <a href="https://unsplash.com/@gabrielkraus?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Gabriel Kraus</a> on <a href="https://unsplash.com/s/photos/sorcery?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>_

We are going to build a little CMS with features including authentication with Sorcery, authorization with Pundit, Users that can create posts and upload images, access a dashboard and manage the navigation menu links and image galleries. We will call it, WizardCMS

![Hagrid You're a wizard harry meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jw786x41j881mt0a7d2e.jpeg)

We are using Sorcery because its low-level nature allows us to create a simple authentication flow with out many of the headaches that other popular libraries are giving other developers.

We have a long road ahead of us, and our passenger coming along for the ride will be our buddy RSpec who knows the way and will make sure we are heading the right direction.

The format of the articles will be that I write a test, layout the code to make the test pass and make the feature/functionality happen, and do my best to explain what is happening in the code. Often I come across article that doesn't explain enough of what is happening and it leaves me wondering why something was done.

**To view progress on this project:**
- [Source code on GitHub](https://github.com/adrianvalenz/wizardcms)
- [Example app on Heroku](https://wizardcms.herokuapp.com/)

## Create new app and install RSpec

_I might be adding my own CSS through the project but I won’t clutter the tutorial with it. You can style and lay out the HTML how you like really. I hope at least to give you a decent reference as to what code you might want to use to create the app with authentication and the other features listed above. I will also be using version control, but I won’t be adding it to the tutorial as well, I’ll leave that up to you. Typically it is a good idea to just create a new branch when working on a new feature or chunk of code. :)_

Create new app `rails new app_name` and [install RSpec](https://adrianvalenz.com/blog/setup-rspec-on-fresh-rails-7-project/)

## Install Sorcery

We'll be installing Sorcery based off this [tutorial](https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication) in their wiki. I'm modifying a little bit since we are creating something different, but also because their tutorial is a bit outdated since it is based off an older version of Rails. 

```
bundle add sorcery
bundle install
rails g sorcery:install
rails db:migrate
```

When we run the migration it will set up a `users` table for us with some attributes to store in the database. 

Let's start or restart the server so Rails loads up Sorcery.

```
rails s 
# or ./bin/dev if using js-bundling/css-bundling
```

Let's go on and add our first feature.

## User cannot access dashboard when logged out

### Spec

```
# spec/system/user_session_flow_spec.rb

require 'rails_helper'

describe "User session flow", type: :system do
  scenario "redirected from dashboard when logged out" do
    visit "/dashboard"
    expect(page).to have_text("You must log in")
    page_title = find('h1#page-title')
    expect(page_title).to have_content("Login")
    expect(page).to have_no_content('Dashboard')
  end

  scenario "user clicks register account" do
    visit dashboard_path
    expect(page).to have_content("Don't have an account?")
    click_link "register-user"
    page_title = find('h1#page-title')
    expect(page_title).to have_content("Register")
  end
end

```

In our first spec file that we will write, I split it into two scenarios because it was getting pretty hefty. You don't want too many things happening in a test because then it becomes difficult to manage. Usually a simple test run where you have a user visit a page, interact with an element, have it make a response and display (or not display) something on a page is enough to find out whether a feature works or not. Here's a simple example:

```
visit "/about"
expect(page).to have_content("About Our Company")
fill_in "Email"
fill_in "Your message"
click_on "Send message"
expect(page).to have_text("You have sent your message successfully.")
```

This example shows me that the user was able to go to the about page and see the title, interact with the form, and get a response that their message was sent. If this passes it means the app has the elements and functionality in place and it is behaving the way we expect it to.

With this in mind, I hope our spec file is self-explanatory just by reading the code.

### Models

```
class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end
```

In our user model file we are declaring that we authenticate a User with the Sorcery gem. Then we lay our a couple validations; these validations are for "virtual attributes" that we will use on our forms. If you noticed when we ran the initial migration there were no `password` or `password_confirmation` attributes in the users table, but instead a `salt` and `crypted_password` attributes. The idea is that you don't store passwords in plain text in the database, so this code will take the virtual attributes in the form and encrypt them before storing them in the database. We also validate the uniqueness of `email`, which means two different user accounts cannot use the same email to sign up, there can only be one: 

![Highlander gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qot324h3dck0huqtiwk.gif)

### Routes

```
# config/routes.rb
get 'register', to: 'user_registrations#new'
get 'login' => 'user_sessions#new', :as => :login
get 'dashboard', to: 'dashboard#show'
root "home#show"
```

Currently we have 4 routes in place, all of which are `get` requests where we request some templates with data. Here we are requesting a register page, login page, dashboard page, and homepage. If you are not familiar with MVC frameworks, these routes actually point to a controller action, which in turn returns the template. The actions can return other things like JSON, but for now we'll focus on templates. Rails returns a template file if it has the same name as the action.

### Controllers

```
# app/controller/dashboard_controller.rb

class DashboardController < ApplicationController
	def show
	end
end
```

Our dashboard route points to the dashboard controller and show action. We don't need to write anything in the show action because by default it'll point to the `show.html.erb` view.

```
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :require_login

  private
  def not_authenticated
    redirect_to login_path, alert: "You must log in"
  end
end
```

The code in our application controller will also be called in the controllers we write. The controllers we write extend from the application controller. In other words, for example, when we write `class DashboardController < ApplicationController; end` we are saying "run the code in the application controller, and if dashboard controller overrides anything, run that".

So by adding `before_action :require_login` to our application controller, it will also be called in every other controller that extends from it unless we say otherwise. That's how access is restricted to the dashboard page and you need to log in to access it.

Sorcery also has a method `not_authenticated` that also runs when a user attempts to access a restricted page, so we specify where we want the user to be redirected and also specify an alert.

```
# app/controllers/home_controller.rb

class HomeController < ApplicationController
	skip_before_action :require_login, only: :show
  def show
  end
end
```

In the home controller, we add another method called `skip_before_action` which basically is "**don't** run this method before any action" and we specify which method by passing in the `:require_login` method, and specify the `show` action by passing it as an option. Essentially this code says the user doesn't need to be logged in to access it.

```
# app/controllers/user_sessions_controller.rb

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[ new ]

  def new
  end
end

```

Earlier in our application controller we called a method, the `login_path` route. The route points to the `user_sessions` controller and it's `new` action. We add the `skip_before_action :require_login` as well so we can give non-signed users the ability to log in. The `new` action will render the `new.html.erb` template.

```
# app/controllers/user_registrations_controller.rb

class UserRegistrationsController < ApplicationController
  skip_before_action :require_login, only: :new

  def new
  end
end
```

### Views

```
# app/views/dashboard/show.html.erb
<h1>Dashboard</h1>
```

```
# app/views/home/show.html.erb
<h1>Homepage</h1>
```

We are starting slow, and only adding the h1 tags with what is the title of the template.

```
# app/views/shared/_navbar.html.erb
<nav>
  <section>
    <%= link_to "Home", root_path %>
    <%= link_to "Dashboard", dashboard_path if current_user %>
  </section>
</nav>

<aside>
  <% if notice %><%= notice %><% end %>
  <% if alert %><%= alert %><% end %>
</aside>

```

We're going to use a conditional statement to display the dashboard link when the user is signed in, and we'll have some methods to show us alerts or notices.

```
# app/views/layouts/application.html.erb

  <body>
    <%= render 'shared/navbar' %>
    <%= yield %>
  </body>
```

Render the `_navbar.html.erb` partial in the `application.html.erb` template.

```
# app/views/user_sessions/new.html.erb

<h1 id="page-title">Login</h1>

<div>
  <p>Don't have an account? <%= link_to "Register", register_path, id: "register-user" %></p>
</div>

```

I like the idea of using Capybara to click on the `id` of an element versus the content of the HTML because I might want to replace "Login" with an SVG icon and add the attribute `title="login"` (Note the title attribute is good practice for accessibility so it's something that is highly recommended to be added).

```
# app/views/user_registrations/new.html.erb
<h1 id="page-title">Register</h1>

```

And lastly the `user_registrations/new.html.erb` template.

After having all these files in place, the test should pass. Awesome, but we are far from done. Let's look at our next feature.

## User registers account

### Spec

```
require 'rails_helper'

describe "User registers account", type: :system do
  scenario "user registers and sees dashboard" do
    visit login_path
    click_link 'register-user'

    email = Faker::Internet.email
    password = Faker::Internet.password
    fill_in "Email", with: email
    fill_in "Password", with: password
    fill_in "Password confirmation", with: password
    click_on "submit-register-user"

    expect(page).to have_text("You have registered successfully")

    page_title = find('h1#page-title')
    expect(page_title).to have_content("Dashboard")
    expect(page).to have_content(email)
  end
end


```

This test is pretty fun. Here we use the `Faker` gem to create a fake user on the fly. From what I understand it is different from using the `FactoryBot` gem method (with user assignment) `user = create :user` because the latter will imply that there already exists a user in the database, the former helps us simulate the creation of a new user when filling out the new user registration form.

### Routes

```
  get 'register', to: 'user_registrations#new'
  post 'register/user', to: 'user_registrations#create'
```

We only need to set up two routes for this test, one to get the form, the other to post the form action. When the user clicks submit it will run the code shown below on the `create` action to create the user.

### Controllers

```
# app/controllers/user_registrations_controller.rb

class UserRegistrationsController < ApplicationController
  skip_before_action :require_login, only: %i[ new create ]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      auto_login(@user)
      redirect_to dashboard_path, notice: "You successfully registered"
    else
      flash.now[:alert] = "Registration failed"
      render :new
    end
  end

  private
  def user_params
	  params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

```

*Fun fact: The `%i` is for creating an output of non-interpolated array of symbols. Here's a [cool blog post](http://ruby.zigzo.com/2014/08/21/rubys-notation/) with some references.*

When someone is filling out the registration form to create a new user account, the `@user` instance variable will load the `new` method on the `User` class. (`@user = User.new`). Then in the form we will fill in the attributes `:email, :password, :password_confirmation` so when we click submit it will take the attributes and use that to create a new user. The attributes are permitted (whitelisted) and required to be present during the `create` action and we pass them via the `user_params` private method. Private methods can only be accessed by other methods if they are within the same class.

What I really like about this particular `create` action that we wrote is that we got to use Sorcery's `auto_login()` method. We pass in the user that was just created and automatically log them in after registering so we can direct them to the dashboard. Clean and simple. From there they can log out and log in as they please, after we create that functionality of course.

### Views

```
# app/views/user_registrations/new.html.erb

<h1 id="page-title">Register</h1>

<%= form_with(model: @user, url: register_user_path, method: :post) do |form| %>

  <div>
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>

  <div>
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

  <div>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
  </div>

  <div>
    <%= form.submit "Create Account", id: "submit-register-user" %>
  </div>
<% end %>

```

This form specifies and loads up the model, the url for the form action which points to our `user_registrations#create` action, and we get explicit about our form submitting a POST method.


```
# app/views/dashboard/show.html.erb
<h1 id="page-title">Dashboard</h1>
```

Now we add the `id` to the h1 tag for the Dashboard because our new test now calls for it. Considering the possibility that Capybara might get "confused" and see the "Dashboard" link in our nav and "Dashboard" title in our page, we add the `id` to make sure it finds the one we want it to find.


```
# app/views/shared/_navbar.html.erb
<nav>
  <section>
    <%= link_to "Home", root_path %>
    <%= link_to "Dashboard", dashboard_path if current_user %>
  </section>
  <section>
    <% if current_user %>
      You are logged in as: <%= current_user.email %>
    <% end %>
  </section>
</nav>

<aside>
  <% if notice %><%= notice %><% end %>
  <% if alert %><%= alert %><% end %>
</aside>

```

Let's up the navbar partial to show the email of the user signed in. Here's how our current navbar looks like, we'll tidy it up later on.

That should be good for that feature.

## User signs in to dashboard

### Spec

```
# spec/system/user_sign_in_spec.rb

require 'rails_helper'

describe "User login flow", type: :system do
  scenario "user goes to log in page" do
    visit root_path
    click_on "sign-in-link"

    page_title = find("h1#page-title")
    expect(page_title).to have_content("Login")
  end

  scenario "user fills out form and signs in" do
    user = create :user

    visit login_path

    fill_in "Email", with: user.email
    fill_in "Password", with: "secret"
    click_on "sign-in-button"

    expect(page).to have_content(user.email)
    expect(page).to have_text('You have logged in successfully')
  end
end

```

Now we are going to make sure an existing user can log in and we'll use `FactoryBot` to test that. While running the test it'll create a new user with predefined attributes which you can refer below in the section titled "Factory".

The first test will make sure the user can click to the login page. Super simple, all we have to do is create the link, but we'll refactor the navbar partial a bit as you'll see in the "Views" section below.

Our second test will have an existing user go to the login page, fill out the form and sign in and see their email displayed on the page and a successful login message.

### Factory

```
# spec/factories/user.rb

FactoryBot.define do
  factory :user do
    sequence(:email) { |i| "user#{i}@example.com" }
    password { "secret" }
    password_confirmation { "secret" }

    salt { salt = "asdasdastr4325234324sdfds" }
    crypted_password { Sorcery::CryptoProviders::BCrypt.encrypt("secret", salt) }
  end
end

```

I'm hoping when we created the User model with Sorcery that it created this factory for you, also refer to my [RSpec setup in Rails 7](https://dev.to/adrianvalenz/setup-rspec-on-a-fresh-rails-7-project-5gp) if you haven't seen it yet.

Let's make our user factory look like the code above.

### Routes
```
# config/routes.rb

post 'login' => "user_sessions#create"
```

This route will point to the create action in the `user_sessions_controller` to create a session for the user. The code to make it happen will be in the following controller section.

### Controllers

```
# app/controllers/user_sessions_controller.rb

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[ new create ]

  def new
  end

  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to(dashboard_path, notice: "You have logged in successfully")
    else
      flash.now[:alert] = "Log in failed"
      render :new
    end
  end
end

```

Sorcery gives us a `login` method where we can pass in our params from the form and it logs us in, if the params exist, or in other words `if @user = true` then it will redirect you to the dashboard or the page you tried to access before being prompted for your login credentials. If the login failed, it will render the login form again and let you know it failed.

### Views

```
# app/views/shared/_navbar.html.erb
<nav>
  <section>
    <%= link_to "Home", root_path %>
  </section>
  <section>
    <% if current_user %>
      <%= link_to "Dashboard", dashboard_path %>
      You are logged in as: <%= current_user.email %>
    <% else %>
      <%= link_to "Login", login_path, id: 'sign-in-link' %>
    <% end %>
  </section>
</nav>

<aside>
  <% if notice %><%= notice %><% end %>
  <% if alert %><%= alert %><% end %>
</aside>

```

We moved the login and log out links into the same conditional statement as the dashboard link because they all pertain to each other.

Run the test and it will pass! On to the next feature!

## User signs out

### Spec

```
# spec/system/user_sign_out_spec.rb

require 'rails_helper'

describe "User signs out", type: :system do
  before do
    user = create :user

    visit login_path

    fill_in "Email", with: user.email
    fill_in "Password", with: "secret"
    click_on "sign-in-button"

    expect(page).to have_content(user.email)
    expect(page).to have_text('You have logged in successfully')
  end
  
  scenario "user signs out from dashboard and get taken to homepage" do
    visit dashboard_path
    expect(page).to have_current_path(dashboard_path)
    click_on 'sign-out-link'
    expect(page).to have_text("You have logged out")
    expect(page).to have_current_path(root_path)
  end
end

```

Now we are going to write a test where we have the user sign out from the dashboard and gets redirected back to the home page. It is implied that the user is already signed in, so we need to reflect that in the test. The `before do` statement will allow us to set up a user before running the tests that we want to pass.

### Routes

```
# config/routes.rb
delete 'logout', to: 'user_sessions#destroy', as: :logout
```

Add the route we'll use to perform the destroy action on a user's session so they can log out.

### Controllers

```
# app/controllers/user_sessions_controller.rb

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[ new create ]

  def new
  end

  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to(dashboard_path, notice: "You have logged in successfully")
    else
      flash.now[:alert] = "Log in failed"
      render :new
    end
  end

  def destroy
    logout
    redirect_to root_path, status: :see_other, notice: "You have logged out"
  end
end

```

### Views

```
# app/views/shared/_navbar.html.erb

<nav>
  <section>
    <%= link_to "Home", root_path %>
  </section>
  <section>
    <% if current_user %>
      <%= link_to "Dashboard", dashboard_path %>
      You are logged in as: <%= current_user.email %>
      <%= link_to "Logout", :logout, id: 'sign-out-link', data: { "turbo-method": :delete } %>
    <% else %>
      <%= link_to "Login", login_path, id: 'sign-in-link' %>
    <% end %>
  </section>
</nav>

<aside>
  <% if notice %><%= notice %><% end %>
  <% if alert %><%= alert %><% end %>
</aside>

```

### Fun facts

So I want to point out a couple things going on here that may look different in comparison to older versions of Rails.

I'll focus on these lines of code:

```
# in the route
delete 'logout', to: 'user_sessions#destroy', as: :logout

# in the controller
redirect_to root_path, status: :see_other, notice: "You have logged out"

# in the view
<%= link_to "Logout", :logout, id: 'sign-out-link', data: { "turbo-method": :delete } %>
```

What you see in the route is how Rails has always laid out their resources. Rails convention is that you map an HTTP verb to a controller action which in turn also maps to a CRUD operation in the database.

Prior to Rails 7, when you would want to log out or destroy a resource (a post or image), you would write `<%= link_to "Sign out", destroy_user_session_path, method: :delete %>`. The `method: :delete` part relied on an old library, [rails-ujs](https://github.com/rails/rails-ujs/tree/master) and it helped make NON-Get requests from hyperlinks. Now with Turbo, the solution is to add `data: { "turbo-method": :delete }` so you don't get an error saying the `link_to` helper was looking for a `GET` resource. However, Turbo is not handling the redirect as we expect it, or at all, we need to explicitly say we want a 303 redirect so we add `status: :see_other` as an option in our `destroy` action.

Being able to control the behavior of my authentication flow with a few controllers and helpers due to the low-level nature of Sorcery really makes the app feel light-weight and I don't feel like I am overriding a ton of code that often comes with another authentication solution.

Run the test and it will pass. Next we are going to refactor some duplicate code and abstract it into a helper.


## Refactor and create a test helper method in spec

In our last feature we added a test where we log in the user in order for them to perform the function to log out. This was the second implementation that we made where we had to have a user signed in to perform some interactions with the web. 

I can already foresee that we are going to need to log in the user multiple throughout the development of the app, and rewriting the same code repeatedly is not a good use of our time. If it was a couple use cases then maybe that's ok, but if you find repetition through out your app, you might want to abstract that code into a helper method that you can call through out the app.

We are going to modify two spec files: `user_sign_in_spec.rb` and `user_sign_out_spec.rb`.

This is the part of the code we will abstract into a test helper method.

```
    visit login_path
    fill_in "Email", with: user.email
    fill_in "Password", with: "secret"
    click_on "sign-in-button"
```

_Note: While there are many ways you can log in a user, currently this is how our app works. We only have one login portal. If we need to update the helper method or create a new one to test a new method of logging in (for example through a social media account) then we would write the code to make it happen._

We'll be creating a new file and modifying the `spec/rails_helper.rb` file.

Create `spec/support/sorcery_test_helper.rb` and add the following code.

```
# spec/support/sorcery_test_helpers_rails.rb

module Sorcery
  module TestHelpers
    module Rails
      def sign_in(user)
        visit login_path
        fill_in "Email", with: user.email
        fill_in "Password", with: "secret"
        click_on "sign-in-button"
      end
    end
  end
end
```

Require this file in the `rails_helper.rb`

```
# spec/rails_helper.rb

# Add additional requires below this line. Rails is not loaded until this point!
require_relative 'support/factory_bot'
require_relative 'support/chrome'
require_relative 'support/sorcery_test_helpers_rails'
```

And include the modules in RSpec's configuration in the same file.

```
# spec/rails_helper.rb
RSpec.configure do |config|
  config.include Sorcery::TestHelpers::Rails
end
```

Now change the spec files to look like this with the new test helper method

```
# spec/system/user_sign_in_spec.rb
require 'rails_helper'

describe "User login flow", type: :system do
	... 

  scenario "user fills out form and signs in" do
    user = create :user
    sign_in(user)

    expect(page).to have_content(user.email)
    expect(page).to have_text('You have logged in successfully')
  end
end

```

```
# spec/system/user_sign_out_spec.rb

require 'rails_helper'

describe "User signs out", type: :system do
  before do
    user = create :user
    sign_in(user)
  end
  
  scenario "user signs out from dashboard and get taken to homepage" do
    visit dashboard_path
    expect(page).to have_current_path(dashboard_path)
    click_on 'sign-out-link'
    expect(page).to have_text("You have logged out")
    expect(page).to have_current_path(root_path)
  end
end

```

We will omit the expectation of seeing the email and success message in our `user_sign_out_spec` since we already know it works from our `user_sign_in_spec`.

Run the test and it should pass!

This article covered some basic authentication features with Sorcery and I think we are at a good point to get familiar with this functionality. Next article we will be creating a reset/forgot password feature, so until if you try out this code don't forget your credentials! 

While other authentication gems do a lot out of the box for you, it tends to do it a very specific way, and often you override things to make it work with your app. With Sorcery, we only add what we need and it is low-level enough to customized how users sign in and out with your app, but high-level enough where you don't have to stress too much about security.

I emailed the current maintainer and he plans to add some new features for Sorcery: "JWT support, WebAuthn integration, and updating the OAuth integration to use Omniauth are the major features slated for v1". 

So that's pretty exciting. Please consider sponsoring the project on [Github](https://github.com/sponsors/athix) at only $5 to show that there is a need for another great authentication solution for the future of Rails.