Lets Build Single Page Application - Part II
In Part I we have setup and configured some basic configuration that needed in order to start the project. Now in this part we will focus on implementing authentication system on the server side(API), which is the foundation part that needed for further implementation. You can find the full source ...
In Part I we have setup and configured some basic configuration that needed in order to start the project. Now in this part we will focus on implementing authentication system on the server side(API), which is the foundation part that needed for further implementation. You can find the full source code of this post in my github repository.
Note The source code the repository contains full implementation of authentication in both server side and client side, but in this post I will show only the server side code.
User Model
Create a user model with attributes, paste in some validation and authentication logic like following.
Column | Type | Key |
---|---|---|
id | integer | primary |
uuid | string | unique |
username | string | unique |
string | unique | |
password_digest | string | none |
api/app/models/user.rb
class User < ActiveRecord::Base include UserCallbacks include Authentication extend FriendlyId friendly_id :username, use: :slugged validates :uuid, uniqueness: true validates :password, length: { in: 6..25 } validates :username, presence: true, uniqueness: true validates :email, presence: true, uniqueness: true , email: true end
api/app/models/concerns/user_callbacks.rb
module UserCallbacks extend ActiveSupport::Concern included do attr_writer :uuid_generator before_create :generate_uuid end private def uuid_generator @uuid_generator ||= SecureRandom end def generate_uuid while !uuid.present? || self.class.exists?(uuid: uuid) self.uuid = uuid_generator.uuid end end end
api/app/models/concerns/authentication.rb
module Authentication extend ActiveSupport::Concern included do has_secure_password end module ClassMethods def authenticate(email, password) user = find_by(email: email) user if user && user.authenticate(password) end end end
What I would like to do is separate out the code into each responsible module as you can see in the code. If you are new to ActiveSupport::Concern module, here is basically what it does:
- Get code in included block and evaluate it in the context of the class that include that module.
- Get code in ClassMethods module and evaluate it in the context of the meta class that include that module. It is as if we wrote
class User class << self # code in module ClassMethods end end
SessionController
Create a controller call session and put in the following code: api/app/controllers/session_controller.rb
class SessionController < ApplicationController before_action :authenticate, only: :destroy before_action :not_login_check, only: :create def create if user = User.authenticate(params[:email], params[:password]) generated_token = AuthenticationToken.instance.generate(user.uuid) user_with_token = UserWithToken.new(token: generated_token, user: user) render json: user_with_token else render_invalid_credential end end def destroy AuthenticationToken.instance.revoke(token) render json: 'Successfully logout.', status: :moved_permanently end private def render_invalid_credential render json: 'Bad credential', status: :moved_permanently end end
I don't want to store authentication token in database that's why User model doesn't have token attribute. AuthenticationToken is a singleton class use to generate and manage authentication token that associate with uuid and store that in redis store, which I will show you later. UserWithToken is a decorator class that decorate on User and add in the token attribute. I used ActiveModel::Serializer to help build API endpoint, so the line with render json: user_with_token will look for a serializer class name UserWithTokenSerializer and generate JSON data.
UserSerializer
The serializer class for User api/app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer attributes :id, :uuid, :email, :username, :slug end
UserWithTokenSerializer
The serializer class for UserWithToken api/app/serializers/user_with_token_serializer.rb
class UserWithTokenSerializer < UserSerializer root 'user' attributes :token end
AuthenticationToken
This is what it looks like api/app/singletons/authentication_token.rb
class AuthenticationToken include Singleton attr_writer :store, :expire, :token_generator def generate(uuid) begin token = token_generator.uuid key = token_key(token) end while store.exists(key) store.set(key, uuid) store.expire(key, expire) token end def uuid(token) store.get token_key(token) end def revoke(token) key = token_key(token) store.del(key) if store.exists(key) end private ....... def token_key(token) "auth_token:#{token}" end end
The generate method will generate a token and use it as a key to store user's uuid in the redis store for later use when user authenticate into our application. It also set expire to 24 hours for each generated key. revoke will delete user's uuid from the store base on the passed in token. uuid will get the user's uuid store in redis back base on passed in token.
UsersController
api/app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :authenticate, except: :create before_action :not_login_check, only: :create def show render json: UserWithToken.new(token: token, user: current_user) end def create @user = User.new(user_params) render_user_errors and return unless @user.save generated_token = AuthenticationToken.instance.generate(@user.uuid) render json: UserWithToken.new(token: generated_token, user: @user) end private def user_params params.require(:user).permit(:username, :email, :password) end def render_user_errors render json: @user.errors, status: :unprocessable_entity end end
There is nothing new here just some basic user registration. In create action we create a new user and if client provide valid information we will log user in by generate token and return token along with new user information back to the client as JSON format.
ApplicationHelper
api/app/helpers/application_helper.rb
module ApplicationHelper def current_user @user ||= authenticate_with_http_token do |token, options| uuid = AuthenticationToken.instance.uuid(token) User.find_by(uuid: uuid) end end def authenticate current_user || render_unauthorized end def token authenticate_with_http_token { |token, options| token } end end
current_user will return currently logged in user if client request with valid authentication token passed into request header. token is here to help us extract token from the header.
Testing
To wrap up this implementation with some testing. I only show some portion of the code because it is too long. You can find the full source code in the github repository that I mention at the beginning of this post.
require 'rails_helper' RSpec.describe SessionController, type: :controller do routes { Api::Engine.routes } describe 'POST create' do let(:credential) { { email: 'user@example.com', password: 'secret' } } before { @user = FactoryGirl.create(:user, credential) } context 'with valid credential' do let(:token) { '50c08a03-f9b1-4abf-bd41-81938e20222e' } let(:user_with_token) { UserWithToken.new(token: token, user: @user) } before do allow(AuthenticationToken.instance).to receive(:generate).and_return(token) post :create, credential, format: :json end it 'should generate token' do expect(AuthenticationToken.instance).to have_received(:generate) end it 'should render user with token as JSON' do expect(response.body).to eq(UserWithTokenSerializer.new(user_with_token).to_json) end end context 'with invalid credential' do let(:invalid_credential) { { email: 'wrong@example.com', password: 'secret' } } before { post :create, invalid_credential, format: :json } it 'should render unauthorized as JSON' do expect(response.body).to eq('Bad credential') expect(response.status).to eq(301) end end end describe 'GET destroy' do context 'logged in' do let(:user) { FactoryGirl.build(:user) } let(:uuid) { '65e4ceb1-6a4d-44ac-a53e-23a8097bcf08' } let(:token) { '50c08a03-f9b1-4abf-bd41-81938e20222e' } before do request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token) allow(User).to receive(:find_by).with({uuid: uuid}).and_return(user) allow(AuthenticationToken.instance).to receive(:uuid).and_return(uuid) allow(AuthenticationToken.instance).to receive(:revoke).with(token) get :destroy, nil, format: :json end it 'should delete token' do puts request.headers expect(AuthenticationToken.instance).to have_received(:revoke).once.with(token) end it 'should generate redirect response as JSON' do expect(response.body).to eq('Successfully logout.') expect(response.status).to eq(301) end end context 'not logged in' do before { get :destroy, nil, format: :json } it 'should render unauthorized as JSON' do expect(response.body).to eq('Access denied') expect(response.status).to eq(401) end end end end
Conclusion
Now that we are done with building API to authenticate the user, in the next part we will focus on implementing the UI using react.