DEV Community

JordanTaylorJ
JordanTaylorJ

Posted on • Edited on

Rails: Resources for a Secure Database

While building my first full stack application with rails, I learned several new skills about working with a database. This includes how to utilize gems and ruby on rails to keep the database secure. Below are my notes about the benefits of strong params, validating data, serializers, error handling, password encryption and cookies.

All code examples are from my BookClub application, built as a space to share and discuss favorite books.

Strong Params

In the controllers I used strong params to define the attributes the controller expects to receive from the frontend. I created a private method of book_params to pass to methods within the controller.

def book_params
    params.permit(:title, :author, :image)
end 
Enter fullscreen mode Exit fullscreen mode

With the following create action Book.create(book_params), a hash containing the attribute :person would return the error: Unpermitted parameters: :person. By only permitting the specified attributes, we can safely use the book_params hash for mass assignment.

Validations

I utilized the built in Ruby validator in my models to protect the database from invalid data. This consists of the validates keyword, an attribute, and a hash of options.

validates :username, presence: true, uniqueness: true
validates :password, presence: true, length: {in: 3..20}
Enter fullscreen mode Exit fullscreen mode

These validations in the User model ensure that there are no duplicated usernames and that a password's length is between three and twenty characters.

  • Active Record validates will only be checked when adding or updating data via Ruby/Rails. The validations will not run with SQL code via the command line. #valid? is the only way to trigger validation without touching the database.

Serializing Associations

The book_serializer.rb defines which attributes are included in the response data. The data for each book is nested three layers deep:

{
    "id": 96,
    "title": "the Sun and Her Flowers",
    "author": "Rupi Kaur",
    "image": "https://images.unsplash.com/photo-1545239351-cefa43af60f3?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=735&q=80",
    "reviews": [
      {
        "id": 261,
        "user_id": 83,
        "book_id": 96,
        "comment": "Impedit quia ex sunt.",
        "favorite": false,
        "user": {
          "id": 83,
          "username": "Ringo"
        }
      }
    ]
}
Enter fullscreen mode Exit fullscreen mode

I utilize macros as well as the include keyword to get all three layers in our response data in order to display the has_many through relationship.

I did not include the timestamps created_at and updated_at, as the frontend will not require this information. The has_many macro is used to attach the reviews to the associated books.

class BookSerializer < ActiveModel::Serializer
  attributes :id, :title, :author, :image, :reviews
  has_many :reviews 
end
Enter fullscreen mode Exit fullscreen mode

By default, AMS only nests associations one level deep. In order to override this behavior, I used the include keyword in books_controller.rb show method:

render json: books, include: 'reviews.user', status: :ok
Enter fullscreen mode Exit fullscreen mode

React Error Handling

To make use of the errors from Ruby, they are rendered to the user in the frontend. All components with a POST and/or PATCH request have errors kept in state

const [errors, setErrors] = useState([]);

After a fetch, we check the response status. If it's okay, we can use the response as intended, otherwise, use setErrors with the error response to display the data.

        .then((r) => {
            if (r.ok){
                r.json().then((r) => setUser(r))
                setErrors([])
            } else {
                r.json().then((r) => setErrors(r.errors))
            }
        })
Enter fullscreen mode Exit fullscreen mode

In the example below, the validation errors are displayed conditionally when invalid data is used to try to create a new user account.

Image description

Password Encryption

The macro has_secure_password comes from the bcrypt gem. It's used to hash and salt all passwords for security.
This gem requires the users table to have the column password_digest. The macro is added to the User model.

Cookies and Sessions

User authentication allows the application to keep track of who is or is not logged in. This is done by setting the user's username to a session hash when the user logs in.

First, we install middleware in our config/application.rb file:

config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.action_dispatch.cookies_same_site_protection = :strict
Enter fullscreen mode Exit fullscreen mode

In the application controller, I included Cookies so that the rest of our controllers can access them. I also defined the method :authorized and set it as a before_action.

class ApplicationController < ActionController::API
  include ActionController::Cookies

  before_action :authorized 

  def authorized 
    return render json: {error: "Not Authorized"}, status: 
    :unauthorized unless session.include? :user_id
  end
end
Enter fullscreen mode Exit fullscreen mode

This authorized method checks sessions to see if a user is currently logged in. The before_action will run the authorized method before every request to check that a user is logged in before proceeding.

Using rails generator, I created a sessions_controller rails g controller Session. Here, I define create and destroy actions for logging in and logging out.

The destroy (logout) is straight forward, simply removing the user_id from session, but the create (login) action needs to be authenticated by a password. The code user&.authenticate(params[:password]) checks that a user was found and the password in params matches what is saved in the database for that user.

 def create
        user = User.find_by(username: params[:username])
        if user&.authenticate(params[:password])
            session[:user_id] = user.id 
            render json: user, status: :created
        else 
            render json: {error: "Invalid username or 
            password"}, status: :unauthorized 
        end 
    end
Enter fullscreen mode Exit fullscreen mode

If the user is found, we save the user.id to the sessions hash.

*** Because the authorized before action was placed in application_controller.rb, it is inherited by all the other controllers. In the sessions_controller.rb, we have to remember to skip the :authorized action for our create (login) route. We can skip :authorized any time we don't need to check that a user is logged in before proceeding with an action.
skip_before_action :authorized, only: :create

I hope these notes give you some insight as to how rails can be used to keep data secure and help you in the creation of your own database.

Happy Coding!

Top comments (0)