In and Out of the Club: A Basic Login Application Using Rails Authentication
Tags: programming, rails, tutorial, react
Published: 2023-04-05 16:16:00

When I was younger I occasionally went to the club on the weekend after a hard week of studying.  When I say club, I am talking about a regular club where people go to dance and mingle as well as imbibe certain beverages.  I usually danced more than I mingled.  When I went to the club, somebody usually checked my identification to verify my identity and either stamped my hand (usually with dye that did not come off for days) or placed a band on my wrist.  

Now, what does going to a loud and sometimes smelly club have to do with logging in and authentication.  As it turns out there are a lot of similarities (except the loud music and the smell).  I will be taking you through the process of signing up a new user, logging in a new user, checking if a user is logged in, and logging out a user.  This requires authentication which means checking that is user is who they say that they are.  It is the same thing as checking your ID at the club.  This process will be explained in excruciating detail.  All the console commands and code will be listed upfront first.  The logic will be illustrated by taking you through the signup, login, checking for logged in user, and logout.  I will explain authentication along the way.  Authorization comes after authentication and will not be discussed in any great detail.  It means allowing the user to do certain things.  It is like getting permission to drink from the bar.

Now we will start working on the backend (server) using Rails.

## **BACKEND (SERVER):**

**Generate models and migrations**

```
rails g model User username password_digest --no-test-framework
      invoke  active_record
      create    db/migrate/20230402074035_create_users.rb
      create    app/models/user.rb
rails db:migrate

== 20230402074035 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0039s
== 20230402074035 CreateUsers: migrated (0.0041s) =============================
```
Here are the migrations.  We have created a users table with the columns `:username`, and `:password_digest`.  I will discuss `:password_digest` shortly.  This is where the encrypted password is stored.

```
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :password_digest

      t.timestamps
    end
  end
end
```
Here is the `User` model.  Note the `has_secure_password` macro.  The `has_secure_password` macro provides two methods to the User model.  These methods are `:password` and `:password_confirmation`.  The users table must have a `password_digest` column for these methods to work (allow a user to enter a new password).  It uses a `before_save` hook to compare `password` and `password_confirmation`, if there is a match then the user is saved and the hashed version of the password is stored in the `password_digest` column.  The `has_secure_password` allows bcrypt to cryptographically hash and salt a password (encryption).  Hashing a password is like putting some fruit in the blender and getting a smoothie.  No matter how hard you try you cannot get the fruit back.  Salting a password involves prepending a random 29-letter string to the hashed password.  This macro also provides password validation with the method `validates_confirmation_of`.  In short, `has_secure_password` allows password storage, encryption and validation.

```
class User < ApplicationRecord
    has_secure_password
end
```
**Generate controllers**

We should start with creating the users controller.  This controller contains two actions that allow a user to sign up with a username and password and check if a user is logged in by seeing if there is a user in session.

The `users#create` action _creates a user_ using the parameters passed from the front end.  `user_params` are strong params because only certain attributes are permitted (`params.permit`) to prevent mass assignment vulnerability.  The validity of the user is checked if the user is valid the the user instance is rendered as JSON, otherwise, you get some error messages.  If the user is valid then the user is signed in that the same time (more on this below).  This is when you are having your ID checked and getting a hand stamp or a wrist bracelet.


![ID check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g5msij61v3z9d7z8br3a.jpg)

The `users#show` action _checks for the presence of a user_.  It finds a user using the id that corresponds to the `user_id` in session.  This will be explained more later, but when a user is logged in an attribute in the session hash (`:user_id`) is set equal to the id of the user that is logged in (`user.id`) using the `session#create` method .  If there is a user logged the user is rendered as JSON, otherwise an error message is rendered.  This is where you can re-enter the club or not after leaving and coming back without have to have another ID check.

```
rails g controller users --no-test-framework
      create  app/controllers/users_controller.rb

class UsersController < ApplicationController

    # POST /signup
    def create
        User.create(user_params)
        if user.valid?
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: {errors: user.errors.full_messages}, status: :unprocessable_entity
        end
    end

    # GET /me
    def show
        user=User.find_by(id: session[:user_id])
        if user
            render json: user, status: :ok
        else
            render json: {error: "Not authorized"}, status: :unauthorized
        end
    end

    private

    def user_params
        params.permit(:username, :password, :password_confirmation)
    end

end
```
Here we create a sessions controller with a `create` action for logging in that responds to a POST /login request, and a `destroy` action for logging out that responds to a DELETE /logout request.

`sessions#create` is responsible for _logging in a user_.  The key step is `session[user_id] = user.id`.  However, there are a few things that need to happen first.  This is where authentication comes into play.  First, let me explain what a session hash is.  A session has is a special type of cookie.  Cookies are information that are sent from the server to the client in a cookies header.  This information is stored in the browser.  Subsequently every time the client makes a request to the server it sends these cookies to the server.  The server can perform actions on these cookies or not.  A session is an encrypted cookie (a serialized hash which is signed with a key).  Regular cookies are stored as plain text in the browser can easily be seen by a user but session cannot since it is encrypted.  Session comes in handy because user information can be stored.

In our `create` method we search for a user using the `username` sent in params.  The second step is checking the password using the `authenticate` method (`user&.authenticate(params[:password])`).  The `&.`  is known in Ruby as the "safe navigation operator".  If user is nil, it will return nil; if not, it will call the `authenticate` method on user. It would be similar to writing `user && user.authenticate(params[:password])`.  The `authenticate` method takes our password, hashes and salts it, and compares it to the hash stored under `password_digest`.  If there is a match then a user_id attribute is created in session (`session[user_id] = user.id`), set equal to the user.id and the user is subsequently signed (also done in the users#create method).  If there is no match then an error message is returned.  Incidentially, this is also done in the `users#create` action as well.  The user is logged in at the time of signup.  This is actually being allowed to enter the club after passing the ID check and being marked.

Logging out is easy.  `sessions#destroy` action deletes the user_id attribute from sessions (`session.delete :user_id`) hence _logging out the user_.  One can also destroy the entire sessions hash by entering `session.destroy` and that would do the job as well.  This is leaving the club and having to come back and get another ID check.

```
rails g controller sessions --no-test-framework
      create  app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

    # POST /login
    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: :unprocessable_entity
        end
    end

    # DELETE /logout
    def destroy
        session.delete :user_id
        head :no_content
    end

end
```
**Create custom routes**

Let's not forget the routes.  We need routes to communicate with our controllers.  A route is an HTTP verb plus a path.  Our routing logic (code) consist of our routes to a controller action.  An action is a method in a controller.  Here is the generic routing logic.  This is important because we need to create **custom** routes.

`HTTP verb '/path', to: controller#action`

Here are our custom routes.  Note that each route communicates with a controller to perform a specific action.

```
Rails.application.routes.draw do
  post '/login', to: 'sessions#create'
  delete 'logout', to: 'sessions#destroy'
  post '/signup', to: 'users#create'
  get '/me', to: 'users#show'
end
```

## **FRONTEND (CLIENT):**

I have tested the backend code using Postman, and so far it works.  Now, we will work on the frontend (client) using React.  Here, I will start talking about the request-response cycle for checking for a logged in user, logging in, signing up a new user, and logging out.

**CHECKING FOR A LOGGED IN USER**

Top level `App` component.  Checks to see if there is a user signed in using the `/me` route.  The URL domain is proxied via `"proxy": "http://localhost:3000"` in our package.json folder.

Here, as soon as `App` is rendered a fetch request is sent to `/me` using the HTTP verb `GET`.  This combination of the HTTP verb `GET` and the path `/me` sends a message to the users controller to execute the show method (`get '/me', to: 'users#show'`) in the User model to see if there is a logged in user.  If there is a user then a JSON response is sent with the user information back to the front end.  The frontend saves the user in state (`setUser(user)`).  The `user` becomes a truthy value and the welcome page of application is displayed.

```
import React from 'react'
import {useState, useEffect} from 'react'
import Login from './Login'
import LogOut from './LogOut'

function App() {
  const [user, setUser] = useState(null)
  const welcomeToTheClub = "https://media.tenor.com/EOQex3fN9-EAAAAC/50cent-club.gif"

  useEffect(() => {
    fetch('/me')
    .then(res=>{
      if (res.ok) {
        res.json().then(user=>setUser(user))
      }}
    )
  }, [])
  if (!user) return <Login setLogin={setUser}/>
  return (
    <div className='welcome-page'>
      <h1>
        Welcome to the Club
      </h1>
      <img src={welcomeToTheClub} alt="50 Cent in the club"/>
      <LogOut setLogin={setUser} />
    </div>
  );
}

export default App;
```
If there is no user then the `user` remains a falsy value and the `Login` page component is displayed. The `Login` Page which uses the `LogInForm` and `SignUpForm` components which will be described further.


![Login Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtk0v66esak6j87btctn.png)

```
import React from 'react'
import LogInForm from './LogInForm'
import SignUpForm from './SignUpForm'
import { useState } from 'react'

function Login({setLogin}) {
    const [wantToSignUp, setWantToSignUp] = useState(false)
  return (
    <div className='form'>
        <LogInForm setLogin={setLogin}/>
        <p>If you do not have an account then click the button below to sign up.</p>
        <button onClick={()=>setWantToSignUp(!wantToSignUp)}>
            {!wantToSignUp? "Sign Up" : "Close sign up form" }
        </button>
        {wantToSignUp ? <SignUpForm setLogin={setLogin}/> : null}
    </div>
  )
}

export default Login
```

**LOGGING IN A USER**

Here is our `LogInForm` component.  Many times there will not be a user logged in and the user will have to log in.  Assume there is a pre-existing user.  This user will log in with a username and password.  The form is submitted.  A fetch is done and a POST request is made to `/login` with the username and password being sent in a params object (in JS/React it is an object, in Ruby it is a hash).


![LogInForm component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gfoyf11u3x7x20cuwgcg.png)

This combination of the HTTP verb `POST` and the path `/login` sends a message to the sessions controller to execute the create method (`post '/login', to: 'sessions#create'`).  The username and password are authenticated as previously described.  If authentication is successful then a JSON response is sent with the user information back to the front end.  The frontend saves the user in state (`setLogin(user)`).  The `user` becomes a truthy value and the welcome page of application is displayed.  If there is no user then the `user` remains a falsy value (null) and error messages are generated on the `Login` page in the `LogInForm` component.

```
import React from 'react'
import {useState} from 'react'

function LogInForm({setLogin}) {
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")
    const [errors, setErrors] = useState(null)

    function handleSubmit(event) {
        event.preventDefault()
        fetch("/login", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({username, password})
        }).then(response => {
            if (response.ok){
                response.json().then(user=>setLogin(user))
            } else {
                response.json().then(errors=>setErrors(errors))
            }
        })
    }
  return (
    <div>
        <h2>Welcome, enter information below to login</h2>
        <form onSubmit={handleSubmit}>
            <label>Enter a username:</label>
            <input 
                type="text" 
                id="username" 
                value={username} 
                onChange={(e)=>setUsername(e.target.value)}
            />
            <br/>
            <label>Enter a password:</label>
            <input 
                type="password" 
                id="password" 
                value={password} 
                onChange={(e)=>setPassword(e.target.value)}
            />
            <br/>
            <button type="submit">Enter</button>
        </form>
        {errors? <p className='error'>{errors.error}</p> : null}
    </div>
  )
}

export default LogInForm
```
**SIGN UP A NEW USER**

Here is our `SignUpForm` component.  This form takes in a username, password, and password_confirmation.  A fetch is done and a POST request is made to `/signup` with the `username`, `password`, and `password_confirmation` being sent in a params object.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukrixumf04nm6vf434qh.png)

This combination of the HTTP verb `POST` and the path `/signup` sends a message to the users controller to execute the create method (`post '/signup', to: 'users#create'`). A user is created with the params passed from the from end. If the user is valid a JSON response is sent with the user information back to the front end.  In this case the user is also logged in at the time of signup by creating a `:user_id` attribute in the session hash and setting that value equal to `user.id`.  The frontend saves the user in state (`setLogin(user)`).  The `user` becomes a truthy value and the welcome page of application is displayed.  If there is no user then the `user` remains a falsy value (null) and error messages are generated on the `Login` page in the `SignUpForm` component.

```
import React from 'react'
import {useState} from 'react'

function SignUpForm({setLogin}) {
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")
    const [errors, setErrors] = useState(null)

    function handleSubmit(event) {
        event.preventDefault()
        fetch("/signup", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({
                username, 
                password, 
                password_confirmation: passwordConfirmation
            })
        }).then(response => {
            if (response.ok){
                response.json().then(user=>setLogin(user))
            } else {
                response.json().then(errors=>setErrors(errors))
            }
        })
    }
  return (
    <div>
        <form onSubmit={handleSubmit}>
            <label>Enter a username:</label>
            <input 
                type="text" 
                id="username" 
                value={username} 
                onChange={(e)=>setUsername(e.target.value)}
            />
            <br/>
            <label>Enter a password:</label>
            <input 
                type="password" 
                id="password" 
                value={password} 
                onChange={(e)=>setPassword(e.target.value)}
            />
            <br/>
            <label>Confirm password:</label>
            <input 
                type="password" 
                id="password_confirmation" 
                value={passwordConfirmation} 
                onChange={(e)=>setPasswordConfirmation(e.target.value)}
            />
            <button type="submit">Enter</button>
        </form>
        {errors? <p className='error' >{errors.errors}</p> : null}
    </div>
  )
}

export default SignUpForm
```
If there is a logged in user, successful login, or successful signup then you will see the welcome page which looks like this.

![Welcome Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9yaeonabvc1wpw359t4.png)

You have now entered the club.  Note the log out button at the bottom which leads to our next topic.

**LOG OUT A USER**

Here is the `LogOut` Component.  A fetch is done and a DELETE request is made to `/logout`.

This combination of the HTTP verb `DELETE` and the path `/logout` sends a message to the sessions controller to execute the destroy method (`delete '/logout', to: 'sessions#destroy'`).  The `user_id` attribute of the sessions hash is destroyed and no JSON is sent.  This signs out the user on the backend.  The `user` is set to null in the frontend and once again becomes a falsy value and the user is logged out and the login page of application is displayed.


```
import React from 'react'

function LogOut({setLogin}) {
    function signOut() {
        fetch('/logout', {
            method: 'DELETE'
        })
        setLogin(false)
    }
  return (
    <div>
        <p>Click the button below to logout</p>
        <button onClick={()=>signOut()}>Log Out</button>
    </div>
  )
}

export default LogOut
```
This is how I have constructed a basic log in application.  Make sure that your application is configured to use cookies and that the `bcrypt` gem is installed.  

**In summary:**

For each of these actions you need a custom route (appropriate HTTP verb and path) which maps to a controller action in the backend.  On the front end the appropriate fetch request to that route must be made.

- To log in: post '/login', to: 'sessions#create'
- To log out: delete 'logout', to: 'sessions#destroy'
- To signup a new user: post '/signup', to: 'users#create'
- To check for a signed in user: get '/me', to: 'users#show'

I hope that you have gained a better understanding of how to use Rails/React to sign up users, log in users, check if a user is logged in, and log out a user.  This is also a good way to illustrate the request-response cycle and talk about authentication.  Thank you for you patience.  I'm leaving the club.  Peace out.