Authentication problem in Ruby Sinatra web app - ruby

I have a simple Sinatra app with two controllers and api helper
# ApplicationController
class ApplicationController < Sinatra::Base
register Sinatra::ActiveRecordExtension
helpers ApiHelper
configure :production, :development do
enable :logging
end
before do
content_type :json
end
get '/hello' do
{ message: 'Hello!' }.to_json
end
end
# ArticlesController
class ArticlesController < ApplicationController
before do
authenticate!
end
get '/articles' do
articles = Article.all
articles.map { |article| serialize(article) }
end
...
end
# ApiHelper
module ApiHelper
def authenticate!
halt 403, { message: 'Unauthorized!' }.to_json unless authorized?
end
private
def authorized?
request.env['HTTP_AUTHORIZATION'] == 'Bearer 123qweasd'
end
end
When I do
curl -X GET -i -H 'Accept: application/json' http://localhost:4567/hello
to do helth check I get 403 Unauthorized. Why? I don't require authentication in /hello endpoint, only in /articles CRUD endpoints so I don't understand why it authenticates in /hello. According to the docs before block is used to perform some action before other action runs but I don't call authenticate! in before block in ApplicationController. What am I missing?

It turned out that not knowingly I was using ArticlesController as a middleware. My config.ru looked like this.
run ApplicationController
use ArticlesController
which made it so that the authenticate! was called before every request.
I changed my config.ru to this:
map '/' do
run ApplicationController
end
map '/articles' do
run ArticlesController
end
And it works.

Related

Rails 5 API custom authentication: 'skip_before_action' on GET routes

I've used this series as a starting point for a Rails backend for a work portfolio website. Adapting it has been mostly straightforward, and it's doing what I want it to. The one big problem is that the 'index' and 'show' (read actions) should be available without authentication, while 'create', 'update', and 'delete' (write actions) should require a valid JWT.
Following the approach used to exclude the signup and login routes from authentication, I've tried
skip_before_action :authorize_request, only: [:index, :show]
in the appropriate controller. This will however crash the application, with
NoMethodError (undefined method `works' for nil:NilClass):
app/controllers/works_controller.rb:10:in `index'
While the problem seems apparent - if skipping the authentication action the class doesn't get instantiated - the fix isn't, to me at least. Could anyone please help?
The code for the project is here.
Application controller
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
# called before every action on controllers
before_action :authorize_request
attr_reader :current_user
private
# Check for valid request token and return user
def authorize_request
#current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
end
end
'Works' controller
class WorksController < ApplicationController
#skip_before_action :authorize_request, only: [:index, :show]
before_action :set_work, only: [:show, :update, :destroy]
# GET /works
def index
#works = current_user.works
json_response(#works)
end
# POST /works
def create
#work = current_user.works.create!(work_params)
json_response(#work, :created)
end
# GET /works/:id
def show
json_response(#work)
end
# PUT /works/:id
def update
#work.update(work_params)
head :no_content
end
# DELETE /works/:id
def destroy
#work.destroy
head :no_content
end
private
def work_params
# whitelist params
params.permit(:title, :nature, :role, :client, :timeframe, :description, :images, :url, :blog_post)
end
def set_work
#work = Work.find(params[:id])
end
end
'Users' controller
class UsersController < ApplicationController
skip_before_action :authorize_request, only: :create
def create
user = User.create!(user_params)
auth_token = AuthenticateUser.new(user.username, user.password).call
response = { message: Message.account_created, access_token: auth_token }
json_response(response, :created)
end
def show
json_response(username: current_user.username)
end
private
def user_params
params.permit(
:username,
:password,
:password_confirmation
)
end
end
'Authentication' controller
class AuthenticationController < ApplicationController
skip_before_action :authorize_request, only: :authenticate
# return auth token once user is authenticated
def authenticate
auth_token =
AuthenticateUser.new(auth_params[:username], auth_params[:password]).call
json_response(access_token: auth_token)
end
private
def auth_params
params.permit(:username, :password)
end
end
'AuthenticateUser' helper
class AuthenticateUser
def initialize(username, password)
#username = username
#password = password
end
# Service entry point
def call
JsonWebToken.encode(user_id: user.id) if user
end
private
attr_reader :username, :password
# verify user credentials
def user
user = User.find_by(username: username)
return user if user && user.authenticate(password)
# raise Authentication error if credentials are invalid
raise(ExceptionHandler::AuthenticationError, Message.invalid_credentials)
end
end
'AuthorizeApiRequest' helper
class AuthorizeApiRequest
def initialize(headers = {})
#headers = headers
end
# Service entry point - return valid user object
def call
{
user: user
}
end
private
attr_reader :headers
def user
# check if user is in the database
# memoize user object
#user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
# handle user not found
rescue ActiveRecord::RecordNotFound => e
# raise custom error
raise(
ExceptionHandler::InvalidToken,
("#{Message.invalid_token} #{e.message}")
)
end
# decode authentication token
def decoded_auth_token
#decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
# check for token in `Authorization` header
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
end
raise(ExceptionHandler::MissingToken, Message.missing_token)
end
end
'ExceptionHandler' helper
module ExceptionHandler
extend ActiveSupport::Concern
# Define custom error subclasses - rescue catches `StandardErrors`
class AuthenticationError < StandardError; end
class MissingToken < StandardError; end
class InvalidToken < StandardError; end
included do
# Define custom handlers
rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request
rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two
rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two
rescue_from ActiveRecord::RecordNotFound do |e|
json_response({ message: e.message }, :not_found)
end
end
private
# JSON response with message; Status code 422 - unprocessable entity
def four_twenty_two(e)
json_response({ message: e.message }, :unprocessable_entity)
end
# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end
end
The error message states:
NoMethodError (undefined method `works' for nil:NilClass):
app/controllers/works_controller.rb:10:in `index'
Or to translate that, on line 10 of the works_controller.rb file, we're calling a method called works on nil, which is throwing an error.
Assuming line 10 of the works_controller is
#works = current_user.works
Then the error message is telling us that we're calling works on nil, i.e. we have no current_user.
Either where you assign this code is not working properly, or you're accessing this part of the code without signing in and haven't coded around that. Either way, the current_user variable is returning nil and shouldn't be.

No Route Matches DELETE Sessions Api

I've started to create an Api for my rails application. I am currently creating the Sessions Controller for Log in.
But for some reason I am getting this error
Started DELETE "/api/v1/sessions/?auth_token=6157d3673725013ebddbb5e26e8cd64756949110"
for 127.0.0.1 at 2014-08-29 18:54:18 -0700
ActionController::RoutingError (No route matches [DELETE] "/api/v1/sessions"):
I am not understanding why this is happening. Sign Out seems to work perfectly on the actual web application.
I know it may need an ID according to the rake routes but I'm not sure how to implement this.
API CONTROLLER
module Api
module V1
class SessionsController < ApplicationController
skip_before_filter :verify_authenticity_token,
:if => Proc.new { |c| c.request.format == 'application/json' }
respond_to :json
def destroy
sign_out
render :status => 200,
:json => { :success => true,
:info => "Logged Out",
:data => {} }
end
end
end
end
CONTROLLER
class SessionsController < ApplicationController
def destroy
sign_out
redirect_to root_path
end
end
SESSION HELPER
def sign_out
current_user = nil
cookies.delete(:remember_token)
end
ROUTES
### API Routes
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
resources :sessions, only: [:new, :create, :destroy]
end
end
RAKE ROUTES
api_v1_sessions POST /api/v1/sessions(.:format)
api/v1/sessions#create {:format=>"json"}
api_v1_session DELETE /api/v1/sessions/:id(.:format)
api/v1/sessions#destroy {:format=>"json"}
From the documentation
You can use resource instead of the resources routes helper. It's used to create routes for a singular resource that you don't access using IDs.
namespace :api, defaults: {format: 'json'} do
namespace :v1, constraints: ApiConstraints.new(version: 1, default: true) do
resource :session, only: [:new, :create, :destroy]
end
end
which will give you
GET /session/new
POST /session
DELETE /session

Log Out User and Change token to NIL

I've started to create an Api for my rails application. I am currently creating the Sessions Controller for Log in.
Sign Out seems to be working, but I'd really like the to be able to Sign Out the User AND set the users oauth_token equal to NIL.(oauth_token = nil)
I've tried the code below, but it can't seem to find the correct user by using their oauth_token.
Curl Command
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -X DELETE
http://localhost:3000/api/sessions/\?auth_token\=5c147a84cd5418771b9063dddcbfde96d5a8630b
API CONTROLLER
module Api
module V1
class SessionsController < ApplicationController
skip_before_filter :verify_authenticity_token,
:if => Proc.new { |c| c.request.format == 'application/json' }
respond_to :json
def destroy
user = User.find_by_oauth_token(params[:session][:oauth_token])
if user.present?
user.oauth_token = nil
user.save
end
sign_out
render :status => 200,
:json => { :success => true,
:info => "Logged Out",
:data => {} }
end
end
end
end
CONTROLLER
class SessionsController < ApplicationController
def destroy
sign_out
redirect_to root_path
end
end
SESSION HELPER
def sign_out
current_user = nil
cookies.delete(:remember_token)
end
ROUTES
### API Routes
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
resources :sessions, only: [:new, :create, :destroy]
end
end
LOGS
Started DELETE "/api/sessions/?auth_token=5c147a84cd5418771b9063dddcbfde96d5a8630b"
for 127.0.0.1 at 2014-09-01 00:05:37 -0700
Processing by Api::V1::SessionsController#destroy as JSON
Parameters: {"auth_token"=>"5c147a84cd5418771b9063dddcbfde96d5a8630b", "session"=>{}}
User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."oauth_token" IS NULL LIMIT 1
Completed 200 OK in 130ms (Views: 0.4ms | ActiveRecord: 6.4ms)
I was able to figure it out by changing my Curl Command to this:
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -X DELETE
http://localhost:3000/api/sessions -d
"{\"session\":{\"oauth_token\":\"5c147a84cd5418771b9063dddcbfde96d5a8630b\"}}"

How to include configuration in multiple sinatra application

If my Sinatra Application structure is like this. Copied from Sinatra Help
require 'sinatra/base'
class LoginScreen Sinatra::Base
enable :sessions
get('/login') { haml :login }
post('/login') do
if params[:name] == 'admin' && params[:password] == 'admin'
session['user_name'] = params[:name]
else
redirect '/login'
end
end
end
class MyApp Sinatra::Base
# middleware will run before filters
use LoginScreen
before do
unless session['user_name']
halt "Access denied, please login."
end
end
get('/') { "Hello #{session['user_name']}." }
end
The question would be if the two applications require the same configuration such as, helpers, registers and asset-pack. How do I make it consistent between the two application without duplicating the code. Could I do something like this?
require 'sinatra/base'
class LoginScreen Sinatra::Base
include_relative("config_file.rb")
enable :sessions
get('/login') { haml :login }
post('/login') do
if params[:name] == 'admin' && params[:password] == 'admin'
session['user_name'] = params[:name]
else
redirect '/login'
end
end
end
class MyApp Sinatra::Base
# middleware will run before filters
include_relative("config_file.rb")
use LoginScreen
before do
unless session['user_name']
halt "Access denied, please login."
end
end
get('/') { "Hello #{session['user_name']}." }
end
config_file.rb
helpers Sinatra::Helper1
helpers Sinatra::Helper2
helpers Sinatra::Helper3
register Sinatra1
register Sinatra2
register Sinatra3
How about using inheritance?
class MyBase < Sinatra::Base
configure do
helpers Sinatra::Helper1
register Sinatra1
end
end
class LoginScreen < MyBase
end
class MyApp < MyBase
use LoginScreen
end
This way configuration gets shared among all applications that descend from MyBase.

Thin, Sinatra, and intercepting static file request to do CAS authentication

I'm using the casrack-the-authenticator gem for CAS authentication. My server is running Thin on top of Sinatra. I've gotten the CAS authentication bit working, but I'm not sure how to tell Rack to intercept "/index.html" requests to confirm the CAS login, and if the user is not allowed to view the page, return a HTTP 403 response instead of serving the actual page. Does anyone have experience with this? Thanks.
My app:
class Foo < Sinatra::Base
enable :sessions
set :public, "public"
use CasrackTheAuthenticator::Simple, :cas_server => "https://my.cas_server.com"
use CasrackTheAuthenticator::RequireCAS
get '/' do
puts "Hello World"
end
end
My rackup file:
require 'foo'
use Rack::CommonLogger
use Rack::Lint
run Foo
Initial attempt at getting Rack to understand authentication in its file service (comments and thoughts welcome):
builder = Rack::Builder.new do
map '/foo/index.html' do
run Proc.new { |env|
user = Rack::Request.new(env).session[CasrackTheAuthenticator::USERNAME_PARAM]
[401, { "Content-Type" => "text/html" }, "CAS Authentication Required"] unless user
# Serve index.html because we detected user
}
end
map '/foo' do
run Foo
end
end
run builder
Casrack-the-Authenticator will put the CAS information into the Rack session. You can pull that out in another piece of Rack middleware or in your Sinatra app.
The following is for a Rails application, but the concept is similar for Sinatra or a Rack middleware:
# in app/controllers/application_controller.rb:
protected
def require_sign_in!
render :nothing => true, :status => 403 unless signed_in?
end
def signed_in?
current_user.present?
end
def current_user
#current_user ||= Person.find_by_username(session[CasrackTheAuthenticator::USERNAME_PARAM])
end

Resources