Skip to content

Commit

Permalink
Add authentication with JWT tokens in http header.
Browse files Browse the repository at this point in the history
- Need to provide a Authentication header with a JWT token to access events_controller.
- Add API endpoint to create users.
- Add API endpoint to authenticate. - provide email and password to get token back.
  • Loading branch information
brentgreeff committed Jan 24, 2018
1 parent df38a1b commit ad20bbe
Show file tree
Hide file tree
Showing 21 changed files with 405 additions and 47 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ gem 'puma', '~> 3.7'
gem 'paranoia'
gem 'acts_as_human'
gem 'active_model_serializers'
gem 'bcrypt'
gem 'jwt'

# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ GEM
acts_as_human (3.0.2)
arel (8.0.0)
awesome_print (1.8.0)
bcrypt (3.1.11)
builder (3.2.3)
byebug (9.1.0)
case_transform (0.2)
Expand Down Expand Up @@ -81,6 +82,7 @@ GEM
i18n (0.9.0)
concurrent-ruby (~> 1.0)
jsonapi-renderer (0.1.3)
jwt (2.1.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
Expand Down Expand Up @@ -193,9 +195,11 @@ DEPENDENCIES
active_model_serializers
acts_as_human
awesome_print
bcrypt
byebug
factory_girl_rails
guard-rspec
jwt
listen (>= 3.0.5, < 3.2)
mysql2 (>= 0.3.18, < 0.5)
paranoia
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class ApplicationController < ActionController::API
AuthenticationError = Class.new(StandardError)

class ApplicationController < ActionController::API
include RecordInvalidHandler
include RecordNotFoundHandler
include Authentication
end
28 changes: 28 additions & 0 deletions app/controllers/authentications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class AuthenticationsController < ApplicationController

def create
@user = User.find_by(email: params[:email])
invalid_credentials if @user.nil?

if @user.authenticate(params[:password])
render json: {'auth_token' => auth_token}.to_json
else
invalid_credentials
end
end

private

def auth_token
JWT.encode({
user_id: @user.id,
exp: 24.hours.from_now.to_i
},
Rails.application.secrets.secret_key_base
)
end

def invalid_credentials
raise AuthenticationError, 'Invalid credentials'
end
end
34 changes: 34 additions & 0 deletions app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Authentication
extend ActiveSupport::Concern

included do

rescue_from AuthenticationError do |e|
message = { message: e.message }
render json: message, status: :unauthorized
end

attr_reader :current_user

def load_current_user
raise AuthenticationError, 'Token required' if auth_header.blank?
@current_user = User.find( get_user_id )
rescue JWT::DecodeError => e
raise AuthenticationError, 'Invalid token'
end

private

def get_user_id
JWT.decode(auth_header, secret_key)[0]['user_id']
end

def auth_header
request.headers['Authorization']
end

def secret_key
Rails.application.secrets.secret_key_base
end
end
end
11 changes: 6 additions & 5 deletions app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
class EventsController < ApplicationController
before_action :load_current_user
before_action :load_event, only: [:update, :destroy]

def index
render json: Event.all
render json: current_user.events
end

def create
@event = Event.create!(group_params)
@event = current_user.events.create!(event_params)
render json: @event, status: :created
end

def update
@event.update!( group_params )
@event.update!( event_params )
end

def destroy
Expand All @@ -20,7 +21,7 @@ def destroy

private

def group_params
def event_params
params.require(:event).permit(
:name, :description, :location, :published,
:starting, :ending, :duration, group_attributes: [
Expand All @@ -31,6 +32,6 @@ def group_params
end

def load_event
@event = Event.find params[:id]
@event = current_user.events.find params[:id]
end
end
13 changes: 13 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class UsersController < ApplicationController

def create
User.create!(user_params)
render json: {message: 'Account created'}, status: :created
end

private

def user_params
params.require(:user).permit(:full_name, :email, :password)
end
end
2 changes: 2 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ class Event < ApplicationRecord

acts_as_paranoid

belongs_to :organiser, class_name: 'User'

belongs_to :group
accepts_nested_attributes_for :group

Expand Down
8 changes: 8 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
class User < ApplicationRecord

has_secure_password

validates :email, presence: true

acts_as_human

has_many :events, foreign_key: 'organiser_id'
end
1 change: 1 addition & 0 deletions app/serializers/event_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ class EventSerializer < ActiveModel::Serializer
:description

belongs_to :group
belongs_to :organiser
end
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@

root to: 'events#index'

post 'login', to: 'authentications#create'

resources :users
resources :events
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AddEmailAndPasswordDigestToUsers < ActiveRecord::Migration[5.1]

def change
add_column :users, :email, :string
change_column_null :users, :email, false

add_column :users, :password_digest, :string
change_column_null :users, :password_digest, false
end
end
7 changes: 7 additions & 0 deletions db/migrate/20180124075452_add_organiser_to_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddOrganiserToEvents < ActiveRecord::Migration[5.1]

def change
add_column :events, :organiser_id, :integer, foreign_key: true
change_column_null :events, :organiser_id, false
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20171025072237) do
ActiveRecord::Schema.define(version: 20180124075452) do

create_table "events", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "name"
Expand All @@ -24,6 +24,7 @@
t.datetime "updated_at", null: false
t.datetime "deleted_at"
t.bigint "group_id"
t.integer "organiser_id", null: false
t.index ["deleted_at"], name: "index_events_on_deleted_at"
t.index ["group_id"], name: "index_events_on_group_id"
end
Expand All @@ -49,6 +50,8 @@
t.string "first_name"
t.string "middle_names"
t.string "last_name"
t.string "email", null: false
t.string "password_digest", null: false
end

add_foreign_key "events", "groups"
Expand Down
1 change: 1 addition & 0 deletions spec/factories/event_factory.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FactoryGirl.define do

factory :event do
organiser
name 'My Birthday Party'
location 'Amirandes Grecotel Exclusive Resort'
starting '2017-10-17'
Expand Down
16 changes: 15 additions & 1 deletion spec/factories/user_factory.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
FactoryGirl.define do

factory :user do
full_name 'Nelson Mandela'
full_name 'kurt russell'
email '[email protected]'
password 'password'

factory :organiser do
full_name 'Carl Cox'
email '[email protected]'
password 'techno'
end

factory :someone_else do
full_name 'Someone Else'
email '[email protected]'
password 'strange'
end
end
end
3 changes: 2 additions & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

require './spec/support/request_spec_helper'
require './spec/support/an_authorized_action'

# Checks for pending migration and applies them before tests are run.
# If you are not using ActiveRecord, you can remove this line.
Expand All @@ -40,7 +41,7 @@
config.include RequestSpecHelper, type: :request

config.include FactoryGirl::Syntax::Methods

# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
Expand Down
66 changes: 66 additions & 0 deletions spec/requests/authentications_request_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'rails_helper'

RSpec.describe "Authentications", type: :request do

describe "POST /authentications" do
let(:user) { create(:user) }

let(:valid) do
{ email: user.email, password: 'password' }
end

let(:app_json) do
{"Content-Type" => "application/json"}
end

context 'logging in' do
before do
expect(JWT).to receive(:encode).with(
{
user_id: user.id,
exp: 24.hours.from_now.to_i
},
Rails.application.secrets.secret_key_base
).and_return 'TOKEN'
end
before { post '/login', params: valid.to_json, headers: app_json }

it 'returns an auth_token' do
expect( json['auth_token'] ).to eq 'TOKEN'
end
end

context 'An email that does NOT exist' do
let(:invalid_email) { valid.merge(email: '[email protected]') }

before { post '/login', params: invalid_email.to_json, headers: app_json }

it 'should error' do
expect( response ).to have_http_status(401)
expect( response.body ).to match /Invalid credentials/
end
end

context 'The wrong password' do
let(:wrong_pass) { valid.merge(password: 'wrong') }

before { post '/login', params: wrong_pass.to_json, headers: app_json }

it 'should error' do
expect( response ).to have_http_status(401)
expect( response.body ).to match /Invalid credentials/
end
end

context 'A blank password' do
let(:blank_pass) { valid.merge(password: '') }

before { post '/login', params: blank_pass.to_json, headers: app_json }

it 'should error' do
expect( response ).to have_http_status(401)
expect( response.body ).to match /Invalid credentials/
end
end
end
end
Loading

0 comments on commit ad20bbe

Please sign in to comment.