Skip to content

Commit

Permalink
Merge pull request #116 from zhishi-project/ft-user-authentication-13…
Browse files Browse the repository at this point in the history
…7599493

#137599493 user authentication
  • Loading branch information
makinwab authored Feb 1, 2017
2 parents 541eb3f + 4f1518f commit 8dbc403
Show file tree
Hide file tree
Showing 30 changed files with 218 additions and 254 deletions.
30 changes: 30 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

GOOGLE_CLIENT_ID: '<client_id>'
GOOGLE_CLIENT_SECRET: '<client_secret>'
BUGSNAG_API_KEY: <api_key>
BASE_ANDELA_URL: '<url>'

development:
SLACK_CLIENT_ID: '<client_id>'
SLACK_CLIENT_SECRET: <client_secret>
ELASTIC_SEARCH_HOST_URL: <host_url>
REDISTOGO_URL: <url>
ZI_NOTIFICATION_URL: <notification_url>
BASE_URL: <base_url>
ANDELA_AUTH_URL: <auth_url>
NEW_RELIC_LICENSE_KEY: <license_key>

staging:
ZI_NOTIFICATION_URL: <notification_url>
BASE_URL: <base_url>
ANDELA_AUTH_URL: <auth_url>

production:
ZI_NOTIFICATION_URL: <notification_url>
BASE_URL: <base_url>
ANDELA_AUTH_URL: <auth_url>

test:
ZI_NOTIFICATION_URL: <notification_url>
BASE_URL: <base_url>
ANDELA_AUTH_URL: <auth_url>
6 changes: 5 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ gem "jwt"
gem 'rack-cors'
# for perfomance and monitoring timeout ensures that when a request is taking too long, it is automatically terminated
# new relic provides a dashboard to view the perfomance of our application
gem "rack-timeout"
gem 'newrelic_rpm'
gem "bugsnag"

# elasticsearch for full text searches
gem 'elasticsearch-model'
gem 'elasticsearch-rails'

gem 'faraday'
gem 'faraday_middleware'

# sidekiq for asynchronous jobs. relevant to enable app to keep functioning even when there are long running jobs
gem 'sidekiq'
gem 'sidekiq-failures'
Expand Down Expand Up @@ -56,9 +58,11 @@ group :test do
gem "json-schema"
gem "mock_redis"
gem 'rspec-sidekiq'
gem 'webmock'
end

group :production, :staging do
gem "rack-timeout"
gem "pg"
gem "rails_12factor"
end
15 changes: 14 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ GEM
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
database_cleaner (1.5.1)
diff-lcs (1.2.5)
docile (1.1.5)
Expand Down Expand Up @@ -88,6 +90,8 @@ GEM
i18n (~> 0.5)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0)
faraday (>= 0.7.4, < 0.10)
ffi (1.9.10)
figaro (1.1.1)
thor (~> 0.14)
Expand All @@ -108,6 +112,7 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
hashdiff (0.3.2)
hashie (3.4.3)
http-cookie (1.0.2)
domain_name (~> 0.5)
Expand Down Expand Up @@ -234,6 +239,7 @@ GEM
rspec-support (3.4.1)
ruby-prof (0.15.9)
ruby_dep (1.3.1)
safe_yaml (1.0.4)
shellany (0.0.1)
shoulda-matchers (3.0.1)
activesupport (>= 4.0.0)
Expand Down Expand Up @@ -278,6 +284,10 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
webmock (2.3.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
will_paginate (3.1.0)

PLATFORMS
Expand All @@ -295,6 +305,8 @@ DEPENDENCIES
elasticsearch-rails
factory_girl_rails
faker
faraday
faraday_middleware
figaro
guard-rspec
jbuilder
Expand Down Expand Up @@ -324,10 +336,11 @@ DEPENDENCIES
sinatra (>= 1.3.0)
spring
sqlite3
webmock
will_paginate

RUBY VERSION
ruby 2.3.1p112

BUNDLED WITH
1.12.5
1.14.1
32 changes: 32 additions & 0 deletions app/authenticators/andela_auth_v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class AndelaAuthV2
attr_reader :response

def self.authenticate(token)
conn = Connection::FaradayConnection.connection(token)
response = conn.get('/api/v1/users/me')
new(response)
end

def initialize(response)
@response = response
end

def body
@body ||= JSON.parse(response.body)
end

def authenticated?
# check response status and body
!body.include? 'error'
end

def current_user
if authenticated?
body
else
raise UserNotFoundError.new('User could not be found')
end
end

class UserNotFoundError < StandardError; end
end
24 changes: 24 additions & 0 deletions app/authenticators/authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require_relative 'andela_auth_v2'

class Authenticator
attr_reader :request
attr_accessor :user

def initialize(request)
@request = request
end

def authenticated?
if request.headers['Authorization']
strategy, token = request.headers['Authorization'].split
auth = AndelaAuthV2.authenticate(token)
if strategy == 'Bearer' && auth.authenticated?
return @user = auth.current_user
else
return false
end
else
return false
end
end
end
23 changes: 23 additions & 0 deletions app/authenticators/connection/faraday_connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Connection::FaradayConnection
BASE_ANDELA_URL = ENV['BASE_ANDELA_URL'] # use env variable to set the url

def self.connection(token)
options = {
headers: {
"Authorization" => "Bearer #{token}",
"Accept" => 'application/json; charset=utf-8',
},
ssl: {
verify: false
}
}

::Faraday::Connection.new(BASE_ANDELA_URL, options) do |conn|
conn.use ::Faraday::Request::Multipart
conn.use ::Faraday::Request::UrlEncoded
conn.use FaradayMiddleware::FollowRedirects
conn.response :logger, Rails.logger
conn.adapter ::Faraday.default_adapter
end
end
end
63 changes: 24 additions & 39 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,43 @@ class ApplicationController < ActionController::API

attr_reader :current_user
helper_method :current_user
before_action :authenticate_user, except: [:login, :logout]

def resource_not_found
not_found = "The resource you tried to access was not found"
render json: {errors: not_found}, status: 404
end
before_action :authenticate_user

def invalid_request(message = error_msg, status = 400)
render json: {errors: message}, status: status
end

def login
user = authenticate_cookie
@current_user = User.from_andela_auth(user) if user && !user['isGuest']
return unauthorized_token unless @current_user

@token = TokenManager.generate_token(@current_user.id)
end

def logout
cookie = request.headers['HTTP_ANDELA_COOKIE']
return invalid_request("Cookie must be provided") unless cookie
CookieHandler.logout_cookie(cookie)
render json: {message: 'logged out'}, status: 200
end

private
def authenticate_user
(authenticate_token && authenticate_cookie) || unauthorized_token
# authenticate_cookie || unauthorized_token
end

def authenticate_token
authenticate_with_http_token do |auth_token, _|
user_id = TokenManager.authenticate(auth_token)['user']
@token = TokenManager.generate_token(user_id) if user_id
end
end

def authenticate_cookie
cookie = request.headers['HTTP_ANDELA_COOKIE']
return unless cookie
user = CookieHandler.validate_with_cookie(cookie)
@current_user = User.find_by(email: user['email']) if user
user
def authenticate_user
auth = Authenticator.new(request)
if auth.authenticated?
create_or_update_user(auth.user)
else
unauthorized_token
end
end

def create_or_update_user(user)
@current_user = User.find_or_create_by(email: user['email'])
# we always want to ensure these attrs are in sync with the auth system
@current_user.update_attributes(
name: user['name'],
image: user['picture'],
active: (user['status'] == 'active')
)
@current_user
end

def unauthorized_token
self.headers['WWW-Authenticate'] = 'Token realm="Application"'
render json: {errors: "Request was made with invalid token"}, status: 401
end

def resource_not_found
not_found = "The resource you tried to access was not found"
render json: {errors: not_found}, status: 404
end

def error_msg
"The operation could not be performed."\
" Please check your request or try again later"
Expand Down
1 change: 0 additions & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ class UsersController < ApplicationController
before_action :set_user, only: [:update, :questions, :tags]
before_action :set_user_with_activities, only: [:activities]
before_action :set_user_with_associations_and_statistics, only: [:show]
skip_before_action :authenticate_user, only: [:login, :authenticate]

def index
users = User.paginate(page: params[:page])
Expand Down
1 change: 0 additions & 1 deletion app/views/application/login.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
json.partial! "users/user", user: @current_user
json.partial! 'tags/tag', tags: @current_user.tags
json.api_key @token

# json.user { json.partial! 'users/user', user: @current_user }
# json.api_key @token
1 change: 0 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
end
end

post "login" => "application#login"
get "logout" => "application#logout"

mount Sidekiq::Web => '/sidekiq'
Expand Down
60 changes: 1 addition & 59 deletions spec/controllers/votes_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,7 @@
create_list(:answer, 5, user: user2)
end

context "when user votes another user's resource" do
before do
request.headers['Authorization'] = "Token token=#{valid_user_token(user2)}"
end

it "upvotes another user's resource" do
# post :upvote, {resource_name: 'questions', resource_id: 1}
# expect(response.body).to eq "{\"response\":1}"
# expect(response.status).to eq 200
end

it "downvotes another user's resource " do
# post :downvote, {resource_name: 'questions', resource_id: 1}
# expect(response.body).to eq "{\"response\":-1}"
# expect(response.status).to eq 200
end
end

context "when user votes own resource" do
before do
request.headers['Authorization'] = "Token token=#{valid_user_token(user2)}"
end

it "doesnt downvote resource" do
# post :downvote, {resource_name: 'answers', resource_id: 1}
# expect(response.body).to include "You can't vote for your post"
# expect(response.status).to eq 403
end

it "doesnt upvote resource" do
# post :upvote, {resource_name: 'answers', resource_id: 1}
# expect(response.body).to include "You can't vote for your post"
# expect(response.status).to eq 403
end
end

context "when user cannot vote on resource" do
before do
request.headers['Authorization'] = "Token token=#{valid_user_token(user3)}"
end

it "doesnt upvotes another user's resource" do
# post :upvote, {resource_name: 'questions', resource_id: 1}
# expect(response.body).to include "Not qualified to vote"
# expect(response.status).to eq 403
end

it "doesnt downvote another user's resource" do
# post :downvote, {resource_name: 'answers', resource_id: 1}
# expect(response.body).to include "Not qualified to vote"
# expect(response.status).to eq 403
end
end

context "when user tries to vote with invalid token" do
before do
request.headers['Authorization'] = "Token token=badtoken"
end

context "when user tries to vote with invalid token", invalid_request: true do
it "rejects invalid token for upvote" do
post :upvote, {resource_name: 'questions', resource_id: 1}
expect(response.body).to include "invalid token"
Expand Down
2 changes: 0 additions & 2 deletions spec/requests/answers/accepting_answer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
let(:answer) { create(:answer, question: question, user: user2) }
let(:path) { accept_question_answer_path(question, answer) }

it_behaves_like "authenticated endpoint", :accept_question_answer_path, 'post', true

describe "validates that question belongs to user" do
let(:question) { create(:question_with_answers, user: user2) }
it "returns unauthorized_access if question doesn't belong to user" do
Expand Down
1 change: 0 additions & 1 deletion spec/requests/answers/answer_request_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require "rails_helper"
require "requests/shared/shared_authenticated_endpoint"

def path_helper(path, answer=false)
question = FactoryGirl.create(:question_with_answers)
Expand Down
Loading

0 comments on commit 8dbc403

Please sign in to comment.