I have a Rails App (that is mainly a JSON API)
I'm Using Devise for authentication using JSON request from whatever source (web , mobile)
and I'm using Simple Token Authentication to authenticate users using HTTP headers.
I'm not sure how the implementation should look like, but I have drafted an implementation that almost works.
There is only one problem. and that is when user tries to sign out... typically it should invalidate the authentication token for the user... but it doesn't, I'm not sure where is the problem really... whether it's with the Devise or the Simple Token Auth... so any help is greatly appreciated.
but here is the code
# router
devise_for :users, controllers: { sessions: 'sessions',
registrations: 'registrations' }
api vendor_string: 'app', default_version: 1, path: '', format: 'json' do
version 1 do
cache as: 'v1' do
resource :some_resource
end
end
the session controller is like this
class SessionsController < Devise::SessionsController
respond_to :json
skip_filter :verify_signed_out_user, only: :destroy
def create
self.resource = warden.authenticate!(scope: resource_name)
render :create, status: :created
end
def destroy
current_user.authentication_token = nil
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ take note of this line
current_user.save!
super
end
end
the previous noted line seems to have a problem... when a user provide a wrong token header, it's still working and the current user refers to the user who shouldn't be authenticated in the first place.. for example here are 2 calls fro, the specs
describe 'DELETE sessions#destroy' do
let(:user) { Fabricate(:confirmed_user) }
let(:auth_token) { user.authentication_token }
describe 'with request headers' do
context 'valid credentials' do
it 'Returns 204' do
delete '/users/sign_out', {}, {
HTTP_CONTENT_TYPE: 'application/json',
HTTP_ACCEPT: "application/vnd.app+json; version=1",
"X-User-Email" => user.email,
"X-User-Token" => user.authentication_token
}
user.reload
expect(response.status).to eq 204
expect(user.authentication_token).not_to eq auth_token
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this is ok cause it's the valid user
end
end
context 'invalid credentials' do
it 'Returns 204' do
delete '/users/sign_out', {}, {
HTTP_CONTENT_TYPE: 'application/json',
HTTP_ACCEPT: "application/vnd.app+json; version=1",
"X-User-Email" => user.email,
"X-User-Token" => 'Invalid'
}
user.reload
expect(response.status).to eq 204
expect(user.authentication_token).to eq auth_token
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this FAILS
# why did the user get new auth token when didn't sign out ????
end
end
end
this is also reported on Github
and for completeness here is the application controller
class ApplicationController < ActionController::Base
# The API responds only to JSON
respond_to :json
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
# default to protect_from_forgery with: :exception
protect_from_forgery with: :null_session
# Token Authenticatable for API
acts_as_token_authentication_handler_for User
end
From simple_authentication_token page on github, the current_user.authentication_token will automatically generated if it was blank (nil) on each time current_user will be saved.
Assuming user is an instance of User, which is token authenticatable: each time user will be saved, and user.authentication_token.blank? it receives a new and unique authentication token (via Devise.friendly_token).
Update
Add acts_as_token_authentication_handler_for User in your sessions_controller.rb
Please read on https://github.com/gonzalo-bulnes/simple_token_authentication/issues/224 . I think that is normal behaviour. You need delete the token on the client side(device)
Related
In my rails UsersController - users#sign_up action, I perform verification to ensure the user has a valid recaptcha v3 token before moving on to the rest of the controller logic. If the recaptcha verification fails then the controller returns and responds with an error message. However, my rspec tests are failing because I am unsure how to mock / bypass the verification in the controller.
spec/requests/auth_spec.rb:
RSpec.describe "Authentication Requests", type: :request do
context "sign up user" do
it "fails to sign up a user without email address" do
headers = { :CONTENT_TYPE => "application/json" }
post "/api/v1/sign_up", :params => { :email => nil, :password => "password123"}.to_json, :headers => headers
expect(response.header['Content-Type']).to include('application/json')
expect(response_body_to_json).to eq({"error"=>"Failed to create user"})
end
end
end
The test is failing when I post to /api/v1/sign_up because there are missing params for the recaptcha token. As far as I understand, it isn't possible to mock a recaptcha v3 token. Therefore it would be preferable to have verify_recaptcha return true for the rspec test.
controllers/api/v1/users_controller:
def sign_up
# Rspec fails here with missing params error
return if !verify_recaptcha('sign_up', recaptcha_params[:token])
#user = User.new(user_credential_params)
if #user.valid?
# Handle success/fail logic
end
end
private
def user_credential_params
params.permit(:email, :password)
end
def recaptcha_params
params.permit(:token)
end
controllers/concerns/users_helper.rb:
def verify_recaptcha(recaptcha_action, token)
secret_key = Rails.application.credentials.RECAPTCHA[:SECRET_KEY]
uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
response = Net::HTTP.get_response(uri)
json = JSON.parse(response.body)
recaptcha_valid = json['success'] && json['score'] > 0.5 && json['action'] == recaptcha_action
if !recaptcha_valid
render :json => { :error_msg => 'Authentication Failure' }, :status => :unauthorized
return false
end
return true
end
Can I stub / mock the verify_recaptcha method that comes from the users_helper concern to return true? Is there a better way to accomplish this?
I did due diligence before asking this question and I found this post: mocking/stubbing a controller recaptcha method with rspec in rails.
This was the answer for that post:
allow(controller).to receive(:verify_recaptcha).and_return(true)
The above didnt work for me because individual had their verify_recaptcha method inside of ApplicationController.rb (which seems a little dirty in my opinion). Given that my verify_recaptcha method is inside of a concern, I am not sure how to access the concern via Rspec.
You can try adding UserController.expects(:verify_recaptcha).returns(true) to your test.
This will bypass the recaptcha or Just try finding where the verify_recaptcha method exists and then write controller or class name before the expect method in
UserController.expects(:verify_recaptcha).returns(true)
I use the gem jwt、devise to build a user login system,
I generate a model Authentication to check the token exist or not.
follow this code:
models/authentication.rb
class Authentication < ApplicationRecord
def self.generate_access_token(email)
payload = {:email => email}
secret = 'secret'
token = JWT.encode payload, secret, 'HS256'
return token
end
end
controllers/user/sessions_controller.rb
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
#token = Authentication.generate_access_token(user.email)
Authentication.create(access_token: #token)
authentications = {token: #token, email: user.email}
render json: authentications, status: :created
else
head(:unauthorized)
end
end
when I do a post request to user/sessions I will get token and user email and store it in localstorage of client, and help me to check the token is valid.
follow this code:
def authenticate_token
token = Authentication.find_by_access_token(params[:token])
head :unauthorized unless token
end
In my question, are there ways to let token don't need to store into database?
You can decode the token and get the email stored in it, and find user by that email.
Suppose you carry the token in the Authorization header, like
Authorization: Bearer <token>
then you can define a before_action to do this:
class ApplicationController < ActionController::API
before_action :authenticate_token
def authenticate_token
token = request.headers['Authorization'].to_s =~ /^Bearer (.*)$/i && $1
return head :unauthorized unless token
payload = JWT.decode(token, 'secret', true, algorithm: 'HS256')
user = User.find_by(email: payload['email'])
return head :unauthorized unless user
# TODO set the `user` as current_user
# How to patch devise's `current_user` helper is another story
end
end
If I were you, I would put user ID in the token, not email, because ID is shorter, and faster to lookup from database, and it exposes nothing personal to the internet (note that JWT is not encrypted. It's just signed).
Or you can skip all these messy things by just using knock instead of devise.
Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?
The short answer is to read this blog post which goes into details of how the concept is minimally implemented
This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...
but for now, here is how I implemented it in some project...
First the angular app part, you can use something like Satellizer which plays nicely...
here is the angular auth module in the front-end app
# coffeescript
config = (
$authProvider
$stateProvider
) ->
$authProvider.httpInterceptor = true # to automatically add the headers for auth
$authProvider.baseUrl = "http://path.to.your.api/"
$authProvider.loginRedirect = '/profile' # front-end route after login
$authProvider.logoutRedirect = '/' # front-end route after logout
$authProvider.signupRedirect = '/sign_in'
$authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
$authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
$authProvider.loginRoute = 'sign_in' # front-end route for login
$authProvider.signupRoute = 'sign_up' # front-end route for sign_up
$authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
$authProvider.tokenRoot = 'data'
$authProvider.tokenName = 'token'
$authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
$authProvider.authHeader = 'Authorization'
$authProvider.authToken = 'Bearer'
$authProvider.storage = 'localStorage'
# state configurations for the routes
$stateProvider
.state 'auth',
url: '/'
abstract: true
templateUrl: 'modules/auth/auth.html'
data:
permissions:
only: ['guest']
redirectTo: 'profile'
.state 'auth.sign_up',
url: $authProvider.signupRoute
views:
'sign_up#auth':
templateUrl: 'modules/auth/sign_up.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
.state 'auth.sign_in',
url: $authProvider.loginRoute
views:
'sign_in#auth':
templateUrl: 'modules/auth/sign_in.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
this is the basic configurations for satellizer... as for the authentication controller... it's something like following
#signIn = (email, password, remember_me) ->
$auth.login
email: email
password: password
remember_me: remember_me
.then(success, error)
return
#signUp = (name, email, password) ->
$auth.signup
name: name
email: email
password: password
.then(success, error)
return
this is the basics for authenticating
as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.
second implement the API controller and the authentication controller
for example it might look something like the following
class Api::V1::ApiController < ApplicationController
# The API responds only to JSON
respond_to :json
before_action :authenticate_user!
protected
def authenticate_user!
http_authorization_header?
authenticate_request
set_current_user
end
# Bad Request if http authorization header missing
def http_authorization_header?
fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
true
end
def authenticate_request
decoded_token ||= AuthenticationToken.decode(authorization_header)
#auth_token ||= AuthenticationToken.where(id: decoded_token['id']).
first unless decoded_token.nil?
fail UnauthorizedError, 'errors.auth.invalid_token' if #auth_token.nil?
end
def set_current_user
#current_user ||= #auth_token.user
end
# JWT's are stored in the Authorization header using this format:
# Bearer some_random_string.encoded_payload.another_random_string
def authorization_header
return #authorization_header if defined? #authorization_header
#authorization_header =
begin
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
else
nil
end
end
end
end
class Api::V1::AuthenticationsController < Api::V1::ApiController
skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]
def sign_in
# getting the current user from sign in request
#current_user ||= User.find_by_credentials(auth_params)
fail UnauthorizedError, 'errors.auth.invalid_credentials' unless #current_user
generate_auth_token(auth_params)
render :authentication, status: 201
end
def sign_out
# this auth token is assigned via api controller from headers
#auth_token.destroy!
head status: 204
end
def generate_auth_token(params)
#auth_token = AuthenticationToken.generate(#current_user, params[:remember_me])
end
end
The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)
here is the implementation for the AuthenticationToken model
class AuthenticationToken < ActiveRecord::Base
## Relations
belongs_to :user
## JWT wrappers
def self.encode(payload)
AuthToken.encode(payload)
end
def self.decode(token)
AuthToken.decode(token)
end
# generate and save new authentication token for the user
def self.generate(user, remember_me = false)
#auth_token = user.authentication_tokens.create
#auth_token.token = AuthToken.generate(#auth_token.id, remember_me)
#auth_token.save!
#auth_token
end
# check if a token can be used or not
# used by background job to clear the authentication collection
def expired?
AuthToken.decode(token).nil?
end
end
it uses a wrapper called AuthToken which wraps the JWT functionality
here is it's implementation
# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def self.decode(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
rescue JWT::ExpiredSignature
# It will raise an error if it is not a token that was generated
# with our secret key or if the user changes the contents of the payload
Rails.logger.info "Expired Token"
nil
rescue
Rails.logger.warn "Invalid Token"
nil
end
def self.generate(token_id, remember_me = false)
exp = remember_me ? 6.months.from_now : 6.hours.from_now
payload = { id: token_id.to_s, exp: exp.to_i }
self.encode(payload)
end
end
I'm trying to build an API using rails to use with an Android App, I've configured Devise and Doorkeeper, but I guess I forgot something...
I have this code in my controller:
doorkeeper_for :all
protect_from_forgery with: :null_session
respond_to :json
In the header
Authorization: Bearer b92d41ffd0723a77fcb8acd03e6e5be3b2f3036f07c9619f5bfb62611e31f42d
When I use GET method it works, but POST or DELETE don't
I get this error when I try to do a POST in my controller
{"error": "You need to sign in or sign up before continuing."}
Any suggestions?
Update
doorkeeper.rb
Doorkeeper.configure do
orm :active_record
resource_owner_authenticator do
current_usuario || warden.authenticate!(:scope => :usuario)
end
resource_owner_from_credentials do |routes|
request.params[:usuario] = {:email => request.params[:username], :password => request.params[:password]}
request.env["devise.allow_params_authentication"] = true
request.env["warden"].authenticate!(:scope => :usuario)
end
end
I have made no changes in default values of Devise
I made Devise authentication to log out via GET, but couldn't make it log out using this Angular.js code:
$scope.logout = ->
$http.get('/users/sign_out').success ->
#If it does not redirect from 'editor' to 'login' then you haven't actually logged out
$location.path('editor')
Devise's logout behaviour seems to be random - sometimes it logs out, sometimes not.
And if I enter /users/sign_out into browser's address bar, it logs out always.
Ok, I switched the Devise authentication's log out to POST request to get rid of caching problems and used following Angular.js code:
$scope.logout = ->
$http.post('/users/sign_out').success ->
$location.path('editor')
The first time it logged out fine, as always, but then I couldn't make it to log out.
I decided to make my own method to see what happens:
match '/logout' => 'api#logout', :via => :post
class ApiController < ApplicationController
before_filter :authenticate_user!
def logout
sign_out
if current_user
puts 'Has not signed out!'
else
puts 'Has signed out!'
end
head :ok
end
end
and detected that after sign_out the current_user is always nil, but then the Angular application by some miracle manages to access other methods of ApiController, and current_user isn't nil there!
I do not understand that. Ok, let us suppose that there may follow some other HTTP request, right after (or at the same time as) logout request, passing the authentication cookie and Devise re-logins, but shouldn't the session ID passed in cookie be expired immediately after call of sign_out method?!
sorry I never responded earlier, hope this helps
My Sesisons Controller
$scope.signOutUser = function () {
$http.delete('/api/users/sign_out', {
auth_token: Session.currentUser // just a cookie storing my token from devise token authentication.
}).success( function(result) {
$cookieStore.remove('_pf_session');
$cookieStore.remove('_pf_name');
$cookieStore.remove('_pf_email');
location.reload(true); // I need to refresh the page to update cookies
}).error( function(result) {
console.log(result);
});
}
My Devise Sessions Controller I overrode
class SessionsController < Devise::SessionsController
before_filter :authenticate_user!, only: :destroy
def destroy
token = params[:auth_token]
#user = User.find_by_authentication_token(token)
#user.reset_authentication_token!
sign_out(#user)
render status: :ok, json: {message: "You have successfully logged out"}
end
end
As you can see, I'm not using Rails cookies and thus my answer may not pertain. If I did I would probably add a line like session[:user] = nil in my destroy action.