I'm having trouble trying to authenticate a request spec. How would I pass a valid auth token in the header of each http request? Is my approach below the correct?
tweets_request_spec.rb
require 'rails_helper'
RSpec.describe 'Tweets API', type: :request do
before do
#tweets = create_list(:tweet, 10)
#tweet = #tweets.first
end
describe 'GET /tweets' do
before { get '/tweets', { "Authorization": *some sort of token*} }
it "returns tweets" do
expect(json).to_not be_empty
expect(json).to eq(10)
end
it "is a successful http request" do
expect(response).to have_http_response(200)
end
end
end
Here is my code for the authentication controller, as well as the modules that help with generating and decoding the auth tokens that are passed in the http headers.
authentication_controller.rb
class AuthenticationController < ApplicationController
skip_before_action :authenticate_request
def authenticate
command = AuthenticateUser.call(params[:email], params[:password])
if command.success?
render json: { auth_token: command.result }
else
render json: { error: command.errors }, status: :authorized
end
end
end
authorize_api_request.rb
class AuthorizeApiRequest
prepend SimpleCommand
def initialize(headers = {})
#headers = headers
end
def call
user
end
private
attr_reader :headers
def user
#user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
#user ||= errors.add(:token, 'Invalid token') && nil
end
#decode the auth token and retrieve the user id
def decoded_auth_token
#decoded_auth_token ||= JSONWebToken.decode(http_auth_header)
end
#retrieve auth token from header
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
end
end
Some Code Extracts copied from the the official pluralsight page
the endpoint to authenticate is in config/routes.rb
post 'authenticate', to: 'authentication#authenticate'
which executes this action. The action returns the token if you correctly authenticate.
def authenticate
command = AuthenticateUser.call(params[:email], params[:password])
if command.success?
render json: { auth_token: command.result }
else
render json: { error: command.errors }, status: :unauthorized
end
end
In rspec you have two options, you either mock this method or create a factory.
The concept of token based authentication is that once authenticated the user will have a token and by providing this token he will be able to access the functionalities only reserved to users
The request
$ curl -H "Content-Type: application/json" -X POST -d '{"email":"example#mail.com","password":"123123123"}' http://localhost:3000/authenticate
gives in response the token
{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA"}
if you include in the header the token, the request will not trigger an authorization error
$ curl -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA" http://localhost:3000/items []
so before doing your get request, include in the request header the token
request.headers['Authorization'] = auth_token
get :your_action
How to provide a correct value of auth_token?
You will need to mock the method authenticate_request in ApplicationController, as it is called before the action
#app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user
private
def authenticate_request
#current_user = AuthorizeApiRequest.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless #current_user
end
end
I believe you should mock this line of code, to avoid receiving an authentication error.
#current_user = AuthorizeApiRequest.call(request.headers).result
so I would write the specs somethind like this
user = FactoryBot.create(:user)
allow(AuthorizeApiRequest).to receive(:call).and_return(user)
# request.headers['Authorization'] = auth_token # this is not required anymore the authentication is skipped
get :your_action
I quote pluralsight
By using before_action, the server passes the request headers (using the built-in object property request.headers) to AuthorizeApiRequest every time the user makes a request. Calling result on AuthorizeApiRequest.call(request.headers) is coming from SimpleCommand module where it is defined as attr_reader :result. The request results are returned to the #current_user, thus becoming available to all controllers inheriting from ApplicationController.
You can read more about mocking at
https://github.com/rspec/rspec-mocks
Related
Trying to set up rails API, getting this error, both through console and in actual API requests:
Rack app error handling request { POST /login } #<NameError: uninitialized constant ActionText::Engine::ApplicationController
This is my ApplicationController:
class ApplicationController < ActionController::API
before_action :authorized
def encode_token(payload)
JWT.encode(payload, 's3cr3t')
end
def auth_header
#requesting the header type of authorization (with token) that we will declare through our api requests
# { Authorization: 'Bearer <token>' }
request.headers['Authorization']
end
def decoded_token
if auth_header
#going to take the token and decode it
# we're only concerned about the first index which will be a token with key of *user_id*
token = auth_header.split(' ')[1]
# header: { 'Authorization': 'Bearer <token>' }
begin
JWT.decode('s3cr3t', true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
end
end
def logged_in_user
#consults decode_token to check the header for valid information
if decoded_token
user_id = decoded_token[0]['user_id']
#user = User.find_by(id: user_id)
end
end
def logged_in?
#returns true or false
!!logged_in_user
end
def authorized
#consults logged_in? see see if user is authorized
render json: { message: 'Please log in' }, status: :unauthorized unless logged_in?
end
end
Of course I would like to sort this error specifically (syntax error?) but not sure how to tackle general errors (beyond status codes) in Rails API. Is there a good practice I should be following?
Thanks!
For NameError: uninitialized constant ActionText::Engine::ApplicationController, where is your ApplicationController defined? It seems like ActionText requires it to be in app/controllers/application_controller.rb and possibly inherited from ActionController::Base.
As you develop an API I don't expect you need ActionText though and just accidentally load it. You should have a look in your config/application.rb file and look what gets loaded. If there is require 'rails/all' you should only load what you really need, e.g.
require "action_controller/railtie"
require "active_record/railtie"
but not sure how to tackle general errors (beyond status codes) in Rails API
In terms of general errors, you can e.g. use a rescue_from like this
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :deny_access # self defined exception
rescue_from ActiveRecord::RecordInvalid, with: :show_errors
rescue_from 'MyAppError::Base' do |exception|
render xml: exception, status: 500
end
private
def deny_access
...
end
def show_errors(exception)
exception.record.new_record? ? ...
end
end
https://apidock.com/rails/ActiveSupport/Rescuable/ClassMethods/rescue_from
For general errors it's not really needed to have a rescue_from StandardError as this is the default behaviour of Rails. Rails has a middleware called PublicExceptions which does (mostly) what you want so you can just let the StandardError propagate.
Instead of { error: "Internal Server Error" } it will render this
{
status: status,
error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500])
}
which in case of an exception will render { status: 500, error: "Internal Server Error" }. This should be a reasonable compromise.
For development you could think about adapting this middleware. You can set it with config.exceptions_app.
https://guides.rubyonrails.org/configuring.html#rails-general-configuration
https://api.rubyonrails.org/classes/ActionDispatch/PublicExceptions.html
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/public_exceptions.rb#L14
I have
class Api::V1::BaseController < ApplicationController
before_filter :authenticate!
private
def authenticate!
session_token = request.env["HTTP_SESSIONTOKEN"]
#current_user = User.where(session_token: session_token).first unless session_token.blank?
unless #current_user
return render_json({message: "ERROR: Invalid Session"})
end
end
def current_user
#current_user
end
end
I am testing session_controller which inherits base_controller
before do
post :create, {email: "raj#gmail.com", user_name: "raj",password: "raj"}
body = JSON.parse(response.body)
#session_token = body["session_token"]
end
describe "PUT #edit_email" do
context "if new email already exist" do
it "should return with a error message" do
put :edit_email, {email: "raj#gmail.com", new_email: "ravi#gmail.com", password: "raj"}
body = JSON.parse(response.body)
expect(body["message"]).to eq("email already exist")
end
end
end
I am new to rspec and here I am confused about calling private authenticate method with session_token.
How to call private method of controller and pass header.Thanks in advance.
In Ruby/Rails and OOP in general private methods should not be tested. You should test the interface, not the implementation of private method.
Implementation could be changed with time - but the interface it provides would (most likely) not.
You can set your env variable like following:
request.env['HTTP_SESSIONTOKEN'] = "..."
For testing your private method:
controller.instance_eval{ authenticate }.should eql ...
I am implementing api via rails.
I want to implement following feature but unable to figure out how?
I tried this sample app
I have user model with email, password and access_token
class UsersController < ApplicationController
def signin
c_user = User.find_by_email(params[:email])
pass = params[:password]
if c_user.password == pass
render json: c_user.access_token
end
end
private
def users_params
params.require(:user).permit(:email, :password)
end
end
If user request via api for http://localhost:3000/signin?email=t1#t.com&password=password
then it will check email and password and return access_token that user can use for future request.
I want to implement same with devise or any other gem please help me to understand it.
Thanks in advance
This is how I emplement such mechanism in my apps:
Generate an access_token whenever a user is created.
Respond with that access_token whenever the user signs in.
Require an access_token authentication for every request needed.
user.rb
class User < ActiveRecord::Base
# Use this before callback to set up User access_token.
before_save :ensure_authentication_token
# If the user has no access_token, generate one.
def ensure_authentication_token
if access_token.blank?
self.access_token = generate_access_token
end
end
private
def generate_access_token
loop do
token = Devise.friendly_token
break token unless User.where(access_token: token).first
end
end
end
application_controller.rb
class ApplicationController < ActionController::Base
private
# To make authentication mechanism more safe,
# require an access_token and a user_email.
def authenticate_user_from_token!
user_email = params[:user_email].presence
user = user_email && User.find_by_email(user_email)
# Use Devise.secure_compare to compare the access_token
# in the database with the access_token given in the params.
if user && Devise.secure_compare(user.access_token, params[:access_token])
# Passing store false, will not store the user in the session,
# so an access_token is needed for every request.
# If you want the access_token to work as a sign in token,
# you can simply remove store: false.
sign_in user, store: false
end
end
end
Then you can use this before_filter in any controller you want to protect with access_token authentication:
before_filter :authenticate_user_from_token!
You also needs to override Devise sessions controller, so it responds with a JSON holding the access_token.
users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
# Disable CSRF protection
skip_before_action :verify_authenticity_token
# Be sure to enable JSON.
respond_to :html, :json
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_flashing_format?
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource) do |format|
format.json { render json: {user_email: resource.email, access_token: resource.access_token} }
end
end
end
And make sure to specify it in your routes:
routes.rb
devise_for :users, controllers: { sessions: 'users/sessions' }
I think the reason you can't do that is because there is no password field as you were expected. There is only encrypted_password in the table.
And you must not save any user's password explicitly in a field like password for security reasons.
The way you can make your api work is by using valid_password? method provided by devise to authenticate the user.
if c_user.valid_password?(params[:password])
... ...
end
I need some help with testing the following. I am doing the RailsCast about securing an api: http://railscasts.com/episodes/352-securing-an-api?view=asciicast
I have a RequestController with a before_filter to check if the request has a token:
class RequestsController < ApplicationController
include ActionController::MimeResponds
include ActionController::HttpAuthentication::Token::ControllerMethods
before_filter :restrict_access
respond_to :json
#...
def authenticate
return restrict_access
end
private
def restrict_access
authenticate_or_request_with_http_token do |token, options|
ApiKey.exists?(access_token: token)
end
end
end
My failing rspec test looks like:
it 'responds successfully to generic request because of key protection' do
api_key = ApiKey.create
api_key.save!
get :index
request.headers["token"] = api_key.access_token
expect(response).to be_success # test for the 200 status-code
end
with result: expected success? to return true, got false
I don't understand how I can inject the valid api_key in to the request so that the response will evaluate to true. Any ideas? Thanks.
Token Authentication expects a HTTP_AUTHORIZATION header in this format:
Token token="my-api-token"
Also, you'll want to set the header before the get :index line:
request.headers["HTTP_AUTHORIZATION"] = "Token token=\"#{api_key.access_token}\""
get :index
You can use the encode_credentials method instead if you prefer:
request.headers["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Token.encode_credentials(api_key.access_token)
I want to allow access to a resource only when the authorization token contained in the HTTP headers matches token stored in the users table.
i'm using curl as shown below to access the resource:
$ curl http://localhost:3000/api/v1/tasks.json -H 'Authorization: Token token="S8S4MPqFNdDz3G1jLsC9"'
in the tasks#index method i would like to check whether the token above matches the current user's authentication token stored in the database.
How can i get the token value as an instance variable so that i can use it as shown below:
def index
#token = ???
if User.exists?(:authentication_token => #token)
### code to access the resource
else
authenticate_user!
end
end
From this railscast, I have found the best solution is to use rails authenticate_or_request_with_http_token method like this:
class Api::V1::TasksController < ApplicationController
before_filter :restrict_access
respond_to :json
def index
#user = User.where("users.authentication_token IS NOT NULL").first
#tasks = #user.tasks
end
private
def restrict_access
authenticate_or_request_with_http_token do |token, options|
User.exists?(authentication_token: token)
end
end