How to use Devise for authentication when using an API - ruby-on-rails

I know there are other questions similar to this, but none of the answers worked under my circumstances. I'm trying to authenticate users through an API. More specifically, I have an iPhone app acting as the "client" and it sends posts requests to a sessions controller I have. Here it is:
class V1::SessionsController < ApplicationController
def create
#user = User.find_by_username(params[:username])
if #user
if #user.valid_password?(params[:password])
sign_in(#user)
render :create
else
render_error_for #user
end
else
render json: {
status: 'ERROR',
data: ['Incorrect Username']
}, status: :unauthorized
end
end
end
I read online that the sign_in(#user) should sign in the user, but that is not working as expected. Additionally, I read that you should reload the user before signing it in like this: #user.reload. This does not work either. I was wondering if maybe this has to do with the fact the I'm not inheriting from the Devise::SessionsController. I learned how to make a sessions controller like this online, so that might be the problem.
The way I'm testing this is by running user.last_sign_in_at in the rails console, but all I'm getting back is nil, which I'm pretty sure means that devise isn't successfully signing in the user. Any help is appreciated, thank you.
UPDATE
def create
#user = User.find_by_username(params[:username])
if #user
if #user.valid_password?(params[:password])
#user.last_sign_in_at = Time.now
render :create
else
render_error_for #user
end
else
render json: {
status: 'ERROR',
data: ['Incorrect Username']
}, status: :unauthorized
end
end
I have found a possible solution, but I haven't marked it as a solution because it seems to me that this solution isn't secure. I'm essentially substituting my own process for creating a session, (and assigning a DateTime to the last_sign_in_at field) in the place of Devise's process. Is this secure? Or is doing this a bad idea? Hard coding Devise's sessions#create action does not work for some reason. I speculate that this has to do with the fact that this is an API and not just a regular website.

As your said More specifically, I have an iPhone app acting as the "client" and it sends posts requests to a sessions controller I have
so I might think that the response you need will include user information and authentication token.
Try this way:
class SessionsController < Devise::SessionsController
skip_before_action :verify_authenticity_token # skip CSRF check for APIs
respond_to :json
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with(resource)
end
def respond_with(resource, opts = {})
render json: resource.as_json(only: [:id,:email, :name, ... ])
.merge!({token: resource.token})
end
end
Regards token, you can check
:jwt_authenticatable, :jwt_revocation_strategy in devise gem.
So response will have user and token: user will be user information and token is authentication token for this user.
Then you need store that token for next time when you call another request to backend, make sure you include it to header as Authorization.
In backend, we get current user based on the authentication token.

You can try out device-token-auth gem which built on top of devise for API authentications.
Please check documentation which describes the usage.

Related

How to set up Token Authentication properly in Rails?

I am trying to make an API for a project. I followed instructions from this video - https://www.youtube.com/watch?v=lgdUqtw4weg&t=165s
Basically in the video, I make a token column for the user model. And set up the controller so I can use a post method.
But when i run the POST.
I get the error saying
{"error":"You need to sign in or sign up before continuing."}
I think Devise is interfering with the POST and sees that the user has not logged in when trying to visit non public pages.
How do I get past this and get the token ?
Here is my api controller.
class Api::V1::UserSerializedController < ApplicationController
protect_from_forgery except: :index
respond_to :json
def create
user = User.where(email: params[:email]).first
if user.valid_password?(params[:encrypted_password])
render json: user.as_json(only: [:email, :authentication_token]),status: :created
else
head(:unauthorized)
end
end
def show
end
end
You are inheriting ApplicationController and i'm guessing you have authenticate_user! before action set there.
You'd have to either change the parent class to something like ApiController (i'd prefer this)
OR
skip this action for your Api::V1::UserSerializedController

Is this Rails JSON authentication API (using Devise) secure?

My Rails app uses Devise for authentication. It has a sister iOS app, and users can log in to the iOS app using the same credentials that they use for the web app. So I need some kind of API for authentication.
Lots of similar questions on here point to this tutorial, but it seems to be out-of-date, as the token_authenticatable module has since been removed from Devise and some of the lines throw errors. (I'm using Devise 3.2.2.) I've attempted to roll my own based on that tutorial (and this one), but I'm not 100% confident in it - I feel like there may be something I've misunderstood or missed.
Firstly, following the advice of this gist, I added an authentication_token text attribute to my users table, and the following to user.rb:
before_save :ensure_authentication_token
def ensure_authentication_token
if authentication_token.blank?
self.authentication_token = generate_authentication_token
end
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.find_by(authentication_token: token)
end
end
Then I have the following controllers:
api_controller.rb
class ApiController < ApplicationController
respond_to :json
skip_before_filter :authenticate_user!
protected
def user_params
params[:user].permit(:email, :password, :password_confirmation)
end
end
(Note that my application_controller has the line before_filter :authenticate_user!.)
api/sessions_controller.rb
class Api::SessionsController < Devise::RegistrationsController
prepend_before_filter :require_no_authentication, :only => [:create ]
before_filter :ensure_params_exist
respond_to :json
skip_before_filter :verify_authenticity_token
def create
build_resource
resource = User.find_for_database_authentication(
email: params[:user][:email]
)
return invalid_login_attempt unless resource
if resource.valid_password?(params[:user][:password])
sign_in("user", resource)
render json: {
success: true,
auth_token: resource.authentication_token,
email: resource.email
}
return
end
invalid_login_attempt
end
def destroy
sign_out(resource_name)
end
protected
def ensure_params_exist
return unless params[:user].blank?
render json: {
success: false,
message: "missing user parameter"
}, status: 422
end
def invalid_login_attempt
warden.custom_failure!
render json: {
success: false,
message: "Error with your login or password"
}, status: 401
end
end
api/registrations_controller.rb
class Api::RegistrationsController < ApiController
skip_before_filter :verify_authenticity_token
def create
user = User.new(user_params)
if user.save
render(
json: Jbuilder.encode do |j|
j.success true
j.email user.email
j.auth_token user.authentication_token
end,
status: 201
)
return
else
warden.custom_failure!
render json: user.errors, status: 422
end
end
end
And in config/routes.rb:
namespace :api, defaults: { format: "json" } do
devise_for :users
end
I'm out of my depth a bit and I'm sure there's something here that my future self will look back on and cringe (there usually is). Some iffy parts:
Firstly, you'll notice that Api::SessionsController inherits from Devise::RegistrationsController whereas Api::RegistrationsController inherits from ApiController (I also have some other controllers such as Api::EventsController < ApiController which deal with more standard REST stuff for my other models and don't have much contact with Devise.) This is a pretty ugly arrangement, but I couldn't figure out another way of getting access the methods I need in Api::RegistrationsController. The tutorial I linked to above has the line include Devise::Controllers::InternalHelpers, but this module seems to have been removed in more recent versions of Devise.
Secondly, I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token. I have my doubts about whether this is a good idea - I see a lot of conflicting or hard to understand advice about whether JSON APIs are vulnerable to CSRF attacks - but adding that line was the only way I could get the damn thing to work.
Thirdly, I want to make sure I understand how authentication works once a user has signed in. Say I have an API call GET /api/friends which returns a list of the current user's friends. As I understand it, the iOS app would have to get the user's authentication_token from the database (which is a fixed value for each user that never changes??), then submit it as a param along with every request, e.g. GET /api/friends?authentication_token=abcdefgh1234, then my Api::FriendsController could do something like User.find_by(authentication_token: params[:authentication_token]) to get the current_user. Is it really this simple, or am I missing something?
So for anyone who's managed to read all the way to the end of this mammoth question, thanks for your time! To summarise:
Is this login system secure? Or is there something I've overlooked or misunderstood, e.g. when it comes to CSRF attacks?
Is my understanding of how to authenticate requests once users are signed in correct? (See "thirdly..." above.)
Is there any way this code can be cleaned up or made nicer? Particularly the ugly design of having one controller inherit from Devise::RegistrationsController and the others from ApiController.
Thanks!
You don't want to disable CSRF, I have read that people think it doesn't apply to JSON APIs for some reason, but this is a misunderstanding. To keep it enabled, you want to make a few changes:
on there server side add a after_filter to your sessions controller:
after_filter :set_csrf_header, only: [:new, :create]
protected
def set_csrf_header
response.headers['X-CSRF-Token'] = form_authenticity_token
end
This will generate a token, put it in your session and copy it in the response header for selected actions.
client side (iOS) you need to make sure two things are in place.
your client needs to scan all server responses for this header and retain it when it is passed along.
... get ahold of response object
// response may be a NSURLResponse object, so convert:
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
// grab token if present, make sure you have a config object to store it in
NSString *token = [[httpResponse allHeaderFields] objectForKey:#"X-CSRF-Token"];
if (token)
[yourConfig setCsrfToken:token];
finally, your client needs to add this token to all 'non GET' requests it sends out:
... get ahold of your request object
if (yourConfig.csrfToken && ![request.httpMethod isEqualToString:#"GET"])
[request setValue:yourConfig.csrfToken forHTTPHeaderField:#"X-CSRF-Token"];
Final piece of the puzzle is to understand that when logging in to devise, two subsequent sessions/csrf tokens are being used. A login flow would look like this:
GET /users/sign_in ->
// new action is called, initial token is set
// now send login form on callback:
POST /users/sign_in <username, password> ->
// create action called, token is reset
// when login is successful, session and token are replaced
// and you can send authenticated requests
Your example seems to mimic the code from the Devise blog - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
As mentioned in that post, you are doing it similar to option 1, which they say is the insecure option. I think the key is that you don't want to simply reset the authentication token every time the user is saved. I think the token should be created explicitly (by some kind of TokenController in the API) and should expire periodically.
You'll notice I say 'I think' since (as far as I can tell) nobody has any more information on this.
The top 10 most common vulnerablites in web applications are documented in the OWASP Top 10. This question mentioned that Cross-Site Request Forgery(CSRF) protection was disabled, and CSRF is on the OWASDP Top 10. In short, CSRF is used by attackers to perform actions as an authenticated user. Disabling CSRF protection will lead to high risk vulnerabilities in an application, and undermines the purpose of having a secure authentication system. Its likely that the CSRF protection was failing, because the client is failing to pass the CSRF synchronization token.
Read the entire OWASP top 10, failing to do so is extremely hazardous. Pay close attention to Broken Authentication and Session Management, also check out the Session Management Cheat Sheet.

Alternative API Token for Controller When Using Devise

I have devise token_auth working fine in my rails 3.2 application. I'm just building an API and need to override the devise authentication mechanism with another token. Why? Because one user has many locations and I want to provide independent access via this token to each location without compromising the whole account.
When a location is created, a location api_token is automatically created.
In my locations controller that I'm trying to access with the new key, I have tried this:
class Api::V1::LocationsController < ApplicationController
before_filter :restrict_access, :only => :index
def index
#locations = Location.all
#page_title = "Locations"
#page_title_content = "A list of all your locations. Click to customise and view reports"
respond_to do |format|
format.json { render json: #locations }
format.xml { render xml: #locations }
end
end
private
def restrict_access
api_key = Location.find_by_api_token(params[:access_token])
head :unauthorized unless api_key
end
end
Everything routes fine however, even when I'm not logged in and don't pass the key in the url, I am permitted to see all locations.
Any suggestions how I can get this working? Also, how can I restrict the locations seen to those with access? Usually I use cancan but can't see how this might work.
With devise you need to use :token_authenticatable and override find_for_token_authentication. Here's what works in my app.
class User
devise :token_authenticatable
self.token_authentication_key = "access_token"
def self.find_for_token_authentication conditions
grant = AccessGrant.where(access_token: conditions[token_authentication_key]).
any_of({access_token_expires_at: nil}, {:access_token_expires_at.gt => Time.now}).
first
if grant
user = User.where(_id: grant.user_id).first
user.current_grant = grant if user
user
end
end
end
And after that you just call the standard authenticate_user! in a before filter
class Api::BaseController < ApplicationController
respond_to :json
# don't protect from forgery here
skip_before_filter :verify_authenticity_token
before_filter :authenticate_user!

Devise rememberable on signup

I'm using devise for the first time in a project so probably this is gonna be a newbie question, I would like to know how to make a user rememberable after signup. Currently I'm logging in the user after signup with something like this:
class UsersController < Devise::RegistrationsController
respond_to :html, :json, :js
def create
if request.format.js? || request.format.json?
build_resource
resource.save
sign_in :user, resource if resource.valid? && request.format.js?
respond_with resource
else
super
end
end
end
Note I have my own version of create since I need to respond to javascript and json requests. Any help would be appreciated.
Thanks in advance!.
I haven't actually tried this yet but you should be able to call resource.remember_me! and that should take care of creating the token and saving it for the given resource.

Rails Security: redirect if not current_user

I've been using authlogic and it works really well. One thing I've noticed is that as a hacker I could easily type in this address:
localhost::3000/users/a_user_that_is_not_me/edit
Since the form in the edit form is for #user which is set to current_user and requires an authenticity token, even if I tried to put in details for the other user I end up changing my own account instead of the other users.
That's nice and good, but I'd like it so that these hackers get redirected before they even see the form.
I tried this in the users_controller:
def edit
if admin?
#user = params[:user]
elsif User.find_by_username(params[:id]) != current_user
#user = current_user
#not_user = User.find_by_username(params[:id])
redirect_to user_path(#not_user)
else
#user = current_user
end
end
The redirect works if I type in an address with another user's name but I get a 404 error when trying to access the edit page for the current user.
Any ideas why this doesn't work?
If you're going to be doing this kind of thing a lot, check out an authorization plugin like authorization-san.
Authorization differs from authentication in that authentication is logging in, but authorization pertains to the authenticated (or un-authenticated) user's rights to perform actions.
With authentication-san, you could define this rule with this piece of code in your controller:
# this assumes you've got some way to set #user to the user you're looking up,
# e.g. in a before_filter
allow_access(:authenticated, :only => [:edit, :update]) { current_user == #user }
It looks like you are assigning #user to a string if the current user is an admin. This is simpler (less typo-prone):
def edit
u = User.find_by_username!(params[:id])
if admin? or current_user.username == params[:id]
#user = u
else
redirect_to user_path(u)
end
end
Also, don't you want to use find_by_username! (with bang on end) so that a 404 page is rendered when the user is not found? I'm not sure how you're getting the 404 page now...

Resources