Simple Authentication for one user in Rails
ArticleLet say you are building really simple website where admin is just one person. Using authentication gem like Devise may be overkill.
There is an option to use Basic Auth in Rails
but Basic Auth has an issue that you cannot Sign Out
source. You can close the browser
or send a 401
response to kill the session in browser but if someone
intercept your Token then he can still use it. So you need too be sure
that your entire app is under https
In Rails you can achive https enforcement with
force_ssl
more here
Anyway in this article we will build really simple one user session solution.
Model
As we are dealing with only one (or few) users we don’t need to store stuff to DB, we will just store the username and password to environment variables and we just use Plain Ruby Object to wrap functionality around comparison and retrieving this values.
# app/models/site_user.rb
class SiteUser
include ActiveModel::Model
attr_accessor :username, :password
def login_valid?
username == ENV['ADMIN_USERNAME'] && password == ENV['ADMIN_PASS']
end
end
In order to load Rails server with the enviroment variable you can start it like:
ADMIN_PASS=bar ADMIN_USERNAME=foo RAILS_ENV=development rails server
Or you can use tool like direnv or gem Figaro to set local ENV variables.
If you want to learn more why storing sensitive data in ENV variable is so crucial I’m recommending this article https://12factor.net/config
Controller, View and Route to create session
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
@site_user = SiteUser.new
end
def create
# sleep 2 # you can add sleep here if you want to slow down brute force attack
# for normal application this is bad idea but for one
# user login no-one care
site_user_params = params.require(:site_user)
@site_user = SiteUser.new
.tap { |su| su.username = site_user_params[:username] }
.tap { |su| su.password = site_user_params[:password] }
if @site_user.login_valid?
session[:current_user] = true
redirect_to '/admin'
else
@site_user.password = nil
flash[:notice] = 'Sorry, wrong credentils'
render 'new'
end
end
end
# app/views/sessions/new.html.erb
<div class="content">
<section>
<div style="color: red;"><%= flash[:notice] if flash[:notice] %></div>
<%= form_for @site_user, url: sessions_path do |f| %>
<div>
<%= f.label :username %>
<%= f.text_field :username %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div>
<%= f.submit 'Log In' %>
</div>
<% end %>
</section>
</div>
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :sessions, only: [:create, :new]
# ...
end
Enforcement of session
class ApplicationController < ActionController::Base
ApplicationNotAuthenticated = Class.new(StandardError)
rescue_from ApplicationNotAuthenticated do
respond_to do |format|
format.json { render json: { errors: [message: "401 Not Authorized"] }, status: 401 }
format.html do
flash[:notice] = "Not Authorized to access this page, plese log in"
redirect_to new_session_path
end
format.any { head 401 }
end
end
def authentication_required!
session[:current_user] || raise(ApplicationNotAuthenticated)
end
end
If you want to use it all you need to do:
# entire controller
class MyController < ApplicationController
before_action :authentication_required!
end
# single action
class MyController < ApplicationController
def show
authentication_required!
@user = User.all
# ...
render :show
end
end
RESTfull logout with DELETE
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :sessions, only: [:create, :new, :destroy]
# ...
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# ...
def destroy
reset_session
redirect_to root_path
end
end
<% if session[:current_user] %>
<li><%= link_to 'Log OUT', session_path('logout'), method: :delete %></li>
<% end %>
Logout with GET
The problem with RESTfull logout is that you need Rails UJS included
otherwise the method: :delete
links will be just GET links. This is
mostly not a problem as you load bunch of Rails lib by default. But if
you are just building a simple website from a downloaded template you
might not necessary load this JS lib.
So here is a solution for GET logout:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# ...
def logout
reset_session
redirect_to root_path
end
end
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :sessions, only: [:create, :new] do
collection do
get :logout
end
end
# ...
end
<% if session[:current_user] %>
<li><%= link_to 'Log OUT', logout_sessions_path %></li>
<% end %>
This way you will end up with logout endpoint localhost:3000/sessions/logout
Rails Admin
If you are using Rails Admin you can implement the authentication method like this:
RailsAdmin.config do |config|
config.authenticate_with do
unless session[:current_user]
flash[:notice] = "Not Authorized to access this page"
redirect_to main_app.new_session_path
end
end
# ....
Sources and Other reading
Entire blog website and all the articles can be forked from this Github Repo