DEV Community

Cover image for You Shall Not Pass: The Ins and Outs of Authentication
Brennan Davis
Brennan Davis

Posted on • Edited on

You Shall Not Pass: The Ins and Outs of Authentication

As I was finishing up my fourth phase at Flatiron School

we learned about how to make a login/signup feature on web applications using an authentication process. This phase in my software development program is centered around the fullstack framework Ruby on Rails. It's quite a power language that can do both frontend and backend programming.

The process to make a website secured through an login or signup authentication process can be very complicated. I definitely had some trouble getting this process down, so I decided the best way to climb this mountain was to write a blog about it to fully conquer authentication.

Ace Venture When Nature Calls

If you are familiar with Ruby on Rails, you probably are know about Gemfiles. They are project dependencies, similar to node modules, that have pre-made code that does a lot of heavy lifting for you, so you can focus on other aspects of your project. We will be using BCrypt.

BCrypt is a hefty gemfile that uses hashes and salts to encrypt passwords in your database. Hashes are a fixed-length value that will always produce the same output for the same given input. The output can be a 32-bit or 64-bit number. What BCrypt does, is add a salt to this hash, which is an additional 29-bit number that generates a new salt every time you login with your password. This method makes it incredibly difficult to hack into the database to reveal the actual password to a user.

Additionally, this gem adds three methods that the user model can use to read, write, and encrypt passwords:

password=
password_confirmation=
authenticate
Enter fullscreen mode Exit fullscreen mode

As there are many ways to authenticate,

this is the method I have found to work (after a lot of troubleshooting). There is a lot to set up, but there is flexibility in how you implement your ruby methods and display your data in the front end. For this example, I am using Ruby v.2.7.4, Rails v.6.1.3 along with React v.17.0.2 and React Dom v.5 for my frontend framework.

Setup Work Flow

  • Install gemfiles

If you are starting a new rails project, these are some important gems to include in your Gemfile:

gem 'bcrypt', '~> 3.1.7'
gem 'rack-cors'
gem 'pg', '~> 1.1'
Enter fullscreen mode Exit fullscreen mode

Something of note is that I use PostgreSQL for my database, and rack-cors as middleware. Rack-cors is usually needed when you are deal with separate frontend and backend projects that work through different domains. Theoretically if you are working on a mono-template, where both ends are in the same parent directory, you won't need rack-cors. When working through this demo, if you encounter any issues related to AJAX or CORS, I would recommend installing rack-cors in your gemfile.

bundle install to install all of your gem files. Additionally, don't forget to npm install all your node modules for your frontend (Use this command in your React folder if your local environment combines both frontend and backend in a parent directory).

Next,

we will need to configure Rails to create cookies and sessions (I highly recommend learning about sessions and how Rails interacts with, more info through the Rails Docs). Inside the configure/application.rb file include the following:

module MyApp
  class Application < Rails::Application
    config.load_defaults 6.1
    config.api_only = true

    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore

    config.action_dispatch.cookies_same_site_protection = :strict
  end
end
Enter fullscreen mode Exit fullscreen mode

The last line that starts with configu.action_dispatch is important to include so that cookies are only shared in the same domain.

Moving on,

we need to add a line of code to our app/controllers/application_controller.rb file:

class ApplicationController < ActionController::API
  include ActionController::Cookies
end
Enter fullscreen mode Exit fullscreen mode

Create

a user model. I prefer using a resource generator to fast-track some of the coding. In your terminal in your project directory, use this command:

rails g resource User name email password_digest 
Enter fullscreen mode Exit fullscreen mode

This will create a user controller, model, serializer, and all CRUD routes in your config/routes.rb file. The column in your user table can be whatever you would like, but the password_digest column is vital to interact with bcrypt.

Add

this to the User Model file:

has_secure_password
Enter fullscreen mode Exit fullscreen mode

This allows up to have proper validations for the password.


Whew, that was a lot to set up. Let's start writing some methods.

Sign Up Work Flow

  • Frontend display a signup form.
  • Client submits form, initiating a POST request to a create method.
  • The backend will make sure the data is properly validated. If it is, it will serialize a newly created object (a user row in the User table). If invalid, the server is provide error messages.

Before we talk about the frontend, let's deal with what the server side will do.

Although not necessary, it is advised to create a custom route for your users#create method, like this:

post '/signup', to: 'users#create'
Enter fullscreen mode Exit fullscreen mode

Using a custom route will allow to continue to use the create method for the user controller if you ever need to.

In your User Controller

create your strong params, to control what the client can send to the database. In a private method, write:

  def user_params
      params.permit(:username, :email, :password, :password_confirmation)
  end
Enter fullscreen mode Exit fullscreen mode

Then write your create method:

  def create
    user = User.create(user_params)
      if user.valid?
        session[:user_id] = user.id # this is the piece that logs a user in and keeps track of users info in subsequent requests.
        render json: user, status: :ok
      else
        render json: user.errors.full_messages, status: :unprocessable_entity
      end
  end
Enter fullscreen mode Exit fullscreen mode

Let's breakdown this method:

  • We first declare a variable, that creates and saves a new User object with the user_params.
  • If the user takes in the valid parameters, it will create a new session, using the id of the newly create user variable. This session will allow the client to stay logged in throughout their entire experience using the web application.
  • If the user is NOT valid, it will render a json object of error messages that can subsequently be rendered by the frontend to display.

On the Frontend

this is how I used my frontend to control a form, use State to create a new object with the form data, and to make a fetch request on submit:

import React, { useState } from "react";

function Signup() {
  const [currentUser, setCurrentUser] = useState({})
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
  });

  const { name, email, password } = formData;

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  }

  function handleSubmit(e) {
    e.preventDefault();
    fetch(`/signup`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(formData),
    }).then((res) => {
      if (res.ok) {
        res.json().then((formData) => {
          setCurrentUser(formData);
        });
      } else {
        res.json().then((errors) => {
          console.error(errors);
        });
      }
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">Username:</label>
      <input
        id="username-signup-input"
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <label htmlFor="email">Email:</label>
      <input
        id="email-signup-input"
        type="text"
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <label htmlFor="password">Password:</label>
      <input
        id="password-signup-input"
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export default Signup;

Enter fullscreen mode Exit fullscreen mode

Important Note:
When I used the fetch in React, I had issues actually creating a session. What fixed this issue was using back-ticks for the URL in your fetch. I would recommend using these back-ticks as precaution.

One additional note about the front end is in the onSubmit function: after the POST request, this function uses a callback function called setCurrentUser that will set state to the newly created user object. That way you can use a useEffect hook to call this state upon initial render of the application. That way your front end will know if the current user is logged in when navigating through the webpage.


To Display User Information

We need to verify that the current user id matches the session id.

In your Application Controller, write a method to check if these two ids match:

    def current_user
        User.find_by(id: session[:user_id])
    end
Enter fullscreen mode Exit fullscreen mode

Great, now let's focus back on the User. Create a new custom route:

get '/me', to: "users#show"
Enter fullscreen mode Exit fullscreen mode

Inside your User model, add the appropriate show method:

def show
  if current_user
    render json: current_user, status: :ok
  else
    render json: "No current session stored", status: :unauthorized
  end
end
Enter fullscreen mode Exit fullscreen mode

Because the current_user method in the Application Controller can be inherited in all children controllers, this method is using the current_user method to create a boolean return of either true or false. If true, then render a json object of the current_user's data.


Log In/Log Out Workflow

This is a similar workflow to the Sign Up process:

  • Write custom routes for the create and destroy for your Sessions Controller
  • Write the appropriate methods in your Sessions Controller.
  • Write the frontend code that practically mimics the Signup component.

If you don't have sessions controller already, use this command in your terminal:

rails g controller Sessions
Enter fullscreen mode Exit fullscreen mode

Logging In...

In your config/routes.rb file:

post "/login", to: "sessions#create"
Enter fullscreen mode Exit fullscreen mode

In your Sessions Controller, write a create method that would create a user variable that uses :params from the form in the frontend to find the appropriate user in the database. If the credentials match up, render a json object of the user variable. If they don't match up, render error messages:

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

Then write your front end to take in data from a controlled form and use a POST request to the /login endpoint. This code should look almost identical to the signup component.

Logging Out...

In your config/routes.rb file:

post "/logout", to: "sessions#destroy"
Enter fullscreen mode Exit fullscreen mode

In your Sessions Controller, write a destroy method that will delete the current session:

  def destroy
    session.delete :user_id
  end
Enter fullscreen mode Exit fullscreen mode

Your front end will fetch a DELETE request to the /logout endpoint.


Star Trek Victory

AND THAT'S A WRAP. Take a couple deep breaths, it's as complicated as you think.

Final Notes

  • Go through this process slowly. Make you sure you understand every part of the code before moving on.
  • Think about work flow process for each feature. Go step by step, from the client side to the server side, to write your code.
  • If you get errors, use Google. Errors are your friend, you WILL debug your way through it.

Hopefully this blog has provided a little bit of clarity to authenticating. Thanks for reading. 'Til next time!

Top comments (0)