DEV Community

Cover image for Setting up Password Authentication With B-Crypt
Erick Vargas
Erick Vargas

Posted on

Setting up Password Authentication With B-Crypt

Phase-4 at FlatIron school brought-forth several new topics including rails, validations, deployment, and authorization. One of the topics I was having trouble with in this phase was password authentication. As such I chose to write my phase-4 blog about the topic. While I had a hard time wrapping my head around this topic, I am writing this to further push my understanding and to give a different point of view, perhaps to someone who is also struggling with the concept. For this example we'll be using rails and react to set up password authentication for users.

Cookies

Before we can begin adding our routes and creating our methods, we need to make sure cookies are enabled in the application configuration file.

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

#This is for security
config.action_dispatch.cookies_same_site_protection = :strict

Enter fullscreen mode Exit fullscreen mode

We will also need to make sure to add include ActionController::Cookies in our application controller in order for all controllers to have access to cookies. Cookies are important because it is what will allow us to store the user in a session.

Login

We'll start with the login route. For a user to log in we will need a route and a post method in the SessionsController, as we are creating a session for the user that will be logged in.
The route we'll use will also be a post request and go in the routes.rb file.

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

In the Sessionscontroller we'll add

class SessionsController < ApplicationController
  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
end
Enter fullscreen mode Exit fullscreen mode

In the code above we are creating a session, where if the password passed in from the params matches the password we have stored in the database, we set the session [:user_id] to the users id.
The front end will look something like this:

function Login({ onLogin }) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    fetch("/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ username, password }),
    })
      .then((r) => r.json())
      .then((user) => onLogin(user));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Above we can see that we are making our post request passing in the username and password.

Staying Logged In

One thing to note is when we refresh the page we lose the value of state which above we have set to password and username. Refreshing the page would log the user out and thus would have to log in again. We can handle this by retrieving the user data from the database once the user logs in. For this we would have to create a separate fetch route, that comes from the UsersController rather than the SessionsController.

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

and in the "UsersController" we add a find method to find the user associated with said account.

class UsersController < ApplicationController
  def show
    user = User.find_by(id: session[:user_id])
    if user
      render json: user
    else
      render json: { error: "Not authorized" }, status: :unauthorized
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

On the front end we can do a fetch request to keep the user logged in.

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/me").then((response) => {
      if (response.ok) {
        response.json().then((user) => setUser(user));
      }
    });
  }, []);
Enter fullscreen mode Exit fullscreen mode

Creating A User

To keep things short B-crypt is a gem that allows us to store our passwords in the database in a secure way. Passwords stored as a hash rather than the real value of the password which would be a security risk. When we create a password in a data-table it isn't stored in a password column, but instead in a column named password_digest. It is important that this column be named password_digest if we want to use B-crypt. In the user model we'll make sure to add has_secure_password in order to hash and salt all passwords.

class User < ApplicationRecord
  has_secure_password
end
Enter fullscreen mode Exit fullscreen mode

The has_secure_password we added also provides two instances for our "User" model which are password and password_confirmation This will allow us to create a new user and add password and password-confirmation fields in a signup form.
*Another bonus is it allows us to raise exceptions to handle any errors.

Just like before we'll add a create method for a signup form in our front end.

class UsersController < ApplicationController
  def create
    user = User.create(user_params)
    if user.valid?
      render json: user, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

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

The params we are permitting for our create method are the username, password and password-confirmation fields which can be seen below.

function SignUp({ onLogin }) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [passwordConfirmation, setPasswordConfirmation] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    fetch("/signup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        username,
        password,
        password_confirmation: passwordConfirmation,
      }),
    })
      .then((r) => r.json())
      .then(onLogin);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Username:</label>
      <input
        type="text"
        id="username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <label htmlFor="password">Password:</label>
      <input
        type="password"
        id="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <label htmlFor="password_confirmation">Confirm Password:</label>
      <input
        type="password"
        id="password_confirmation"
        value={passwordConfirmation}
        onChange={(e) => setPasswordConfirmation(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Deleting a Session
We can now sign in and sign up users, but what if we want to delete the current session or logout? It is very similar, where we'll need a route in our route.rb file, a delete method in our "SessionsController" and a request made from our front end, that will be triggered when we click on a Logout button.

#route
delete "/logout", to: "sessions#destroy"

#method in SessionsController
def destroy
  session.delete :user_id
  head :no_content
end

#react/frontEnd request 
function Navbar({ onLogout }) {
  function handleLogout() {
    fetch("/logout", {
      method: "DELETE",
    }).then(() => onLogout());
  }

  return (
    <header>
      <button onClick={handleLogout}>Logout</button>
    </header>
  );
}

Enter fullscreen mode Exit fullscreen mode

User authentication for phase-4 was by far one of the most challenging topics for me to comprehend at first, but was still very engaging to learn. As I finish phase-4, I am now able to create full-stack applications. However, there is still much to learn and I am eager to see what else there is out there.

Sources:
Accessing A Session
B-Crypt
Has-Secure Password
FlatIronSchool

Top comments (0)