Setting up a hashed password system to prevent internal/external access.

Nikhil Kumar
Winkl Insights
Published in
7 min readApr 19, 2019

--

This is a simple article which will explain how you can go about setting up a simple system to prevent passwords from being accessible to any one. The motive of this post is to show that using rails, it is extremely simple to setup.

I am planning to create a series of posts built upon this post that picks one topic and helps implement it easily, using rails. Let me know what you think. Lets get started:

Here is a Github link to the full code.

Start off by creating a new rails API app with this simple single line command:

rails new new_app --api

Install all gems by running:

gem install bundlebundle

Create a user scaffold:

rails g scaffold user name, email, profile_picture

Your file /db/migrate/*_create_users.rb looks like this:

class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.string :email, index: true
t.string :profile_picture
t.timestamps
end
end
end

Your file /app/models/user.rb looks like this:

class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true, on: :create
end
//validates helps you place conditions on certain fields

Your file /app/controllers/users_controller.rb looks like this:

class UsersController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
# GET /users
def index
@users = User.all
render json: @users
end
# GET /users/1
def show
render json: @user
end
# POST /users
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created, location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
# DELETE /users/1
def destroy
# @user.destroy //prevent delete action ATM
render json: {action: false}
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:name, :email, :profile_picture)
end
end

Create and migrate your database:

rails db:createrails db:migrate

This is all you need for CRUD(Without the D) for a user. We will now create a separate password table to store user passwords. I prefer this method as it helps easily return user objects without the worry of including passwords in the object by mistake. Here is an example:

Dummy function:def get_user
user = User.first //Here I can pick the user without passwords
render json {user: user}
end

Create a user password table:

rails g model UserPassword

Your file /db/migrate/*_create_user_passwords.rb looks like this:

class CreateUserPasswords < ActiveRecord::Migration[5.2]
def change
create_table :user_passwords do |t|
t.belongs_to :user, index: true
t.string :password_hash
t.timestamps
end
end
end

Your file /app/models/user_password.rb looks like this:

class UserPassword < ApplicationRecord
belongs_to :user
end

Update your file /app/models/user.rb to look like this:

class User < ApplicationRecord
has_one :user_password, dependent: :delete
validates :name, presence: true
validates :email, presence: true, uniqueness: true, on: :create
end
//validates helps you place conditions on certain fields

Migrate your user_passwords table:

rails db:migrate

Adding BCRYPT to Gemfile. Bcrypt is used to hash passwords so that they cannot be un-hashed to reveal the password.

It looks like this:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.3.4'# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.3'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false
gem 'bcrypt-ruby', :require => 'bcrypt'# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
group :development do
gem 'listen', '>= 3.0.5', '< 3.2'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Update your gems:

bundle

You will need two specific methods from Bcrypt to generate secure passwords:

The create function is used to generate a hashed password to be used. This password cannot be un-hashed easily.

hashed_password = BCrypt::Password.create("Password", cost: 13) //12 is default//The cost determines how hard it is to un-hash your password. The higher, the more hard. Creating passwords with higher cost take more time.

Now all you need to do is modify your user_controller.rb file to take have login and create APIs to access user data.

Create API: Creates a user and adds their password as a hashed string which cannot be un-hashed.

  def create
@user = User.new(user_params)
if !params['password'].nil?
password = BCrypt::Password.create(params['password'])
if @user.save
up = UserPassword.create(user_id: @user.id, password_hash: password)
render json: @user, status: :created, location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
else
render json: {status: 'no password'}
end
end

Update API: Updates user details based on match of password. The password sent is hashed using a similar process during creation and is compared with the existing hashed password. This will help find if its the same password but we will not know what the exact password is till you compare the password. This makes it way harder to find what the password is.

  def update
@db_password = BCrypt::Password.new(@user.user_password.password_hash)
if @db_password == params['password']
if @user.update(user_params)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
else
render json: {status: 'password missmatch'}
end
end

Login API: Picks the user from the email ID sent and compares the password set for that user with the one that is sent during this request by hashing it and comparing it. Similar to how it was done during update. User object is returned on successful match.

  def login
@existing_user = User.find_by_email(params['email'])
if @existing_user
@db_password = BCrypt::Password.new(@existing_user.user_password.password_hash)
if @db_password == params['password']
render json: @existing_user
else
render json: {status: 'exists'}
return
end
else
render json: {status: false}
return
end
end

Your /app/controllers/user_controller.rb will look like this at the end of it:

class UsersController < ApplicationController
include BCrypt
# POST /users
def create
@user = User.new(user_params)
if !params['password'].nil?
password = Password.create(params['password'])
if @user.save
up = UserPassword.create(user_id: @user.id, password_hash: password)
render json: @user, status: :created, location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
else
render json: {status: 'no password'}
end
end
# PATCH/PUT /users/1
def update
@db_password = Password.new(@user.user_password.password_hash)
if @db_password == params['password']
if @user.update(user_params)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
else
render json: {status: 'password missmatch'}
end
end
def login
@existing_user = User.find_by_email(params['email'])
if @existing_user
@db_password = Password.new(@existing_user.user_password.password_hash)
if @db_password == params['password']
render json: @existing_user
else
render json: {status: 'exists'}
return
end
else
render json: {status: false}
return
end
end
# DELETE /users/1
def destroy
# @user.destroy
render json: {action: false}
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:name, :email, :profile_picture)
end
end

You will need to make some small changes with your /config/routes.rb:

Rails.application.routes.draw do
resources :users
get '/login', to: 'users#login'
end

Testing the app:

rails s

Create User:

User Login:

Hashed Password:

Thanks for reading through. Please feel free to leave your feedback and clap if you found this useful. Subscribe to get more posts like this. Any questions? Please leave it in the comments section below. Will try to be as helpful as possible. You can also follow me on Twitter.

--

--