I have a PhoneGap app with a Rails backend. I'm trying to figure out what the best way is to authenticate a user from the mobile app using json.
I am using devise currently, but I don't have to use that. What would be the most simple way to modify devise to work with a mobile app in Phonegap?
I know there are quite a few posts on this... but, some of them are outdated or seem like very complex hacks. Hoping there may be more up to date info from some tried and tested projects, or tutorials.
One post I found also suggests using jsonp, but it also seemed like a pretty complex hack. You can find it here: http://vimeo.com/18763953
I'm also wondering if I would just be better off starting out with authentication from scratch, as laid out in this Railscast: http://railscasts.com/episodes/250-authentication-from-scratch
Thanks!
You should override devise's sessions and registrations controller. I'll only show you how to override the sessions controller:
First, go to your User model and add the Token Authenticatable module. Something like this:
devise :token_authenticatable
before_save :ensure_authentication_token
Then edit your devise.rb file to configure that module:
# You can skip storage for :http_auth and :token_auth by adding those symbols to the array below.
config.skip_session_storage = [:token_auth]
# Defines name of the authentication token params key
config.token_authentication_key = :auth_token
Now edit your routes and point to your new controllers:
devise_for :users, :controllers => { :registrations => 'registrations', :sessions => 'sessions' }
And then create your controller like this:
class SessionsController < Devise::SessionsController
def create
respond_to do |format|
format.html {
super
}
format.json {
build_resource
user = User.find_for_database_authentication(:email => params[:user][:email])
return invalid_login_attempt unless resource
if user.valid_password?(params[:user][:password])
render :json => { :auth_token => user.authentication_token }, success: true, status: :created
else
invalid_login_attempt
end
}
end
end
def destroy
respond_to do |format|
format.html {
super
}
format.json {
user = User.find_by_authentication_token(params[:auth_token])
if user
user.reset_authentication_token!
render :json => { :message => 'Session deleted.' }, :success => true, :status => 204
else
render :json => { :message => 'Invalid token.' }, :status => 404
end
}
end
end
protected
def invalid_login_attempt
warden.custom_failure!
render json: { success: false, message: 'Error with your login or password' }, status: 401
end
end
Devise has a page about this, but it only points to some already outdated guides. But maybe it will help you.
Related
I am attempting to get a user registration endpoint setup for my rails application so that I can access the app's functionality in an iOS rendition. I've gone ahead and namespaced my API, and so far have managed to get user authentication working using Devise and JWT's.
This is great, however, I also need to ability to register a user via the API. To be frank, I have no idea how to correctly implement this. Several Google searches either bring up outdated articles, use the deprecated token authenticatable, or have never been answered.
Below is the code that I believe pertains most to this question:
routes.rb (Namespaced section for API)
namespace :api do
namespace :v1 do
devise_for :users, controllers: { registrations: 'api/v1/registrations' }
resources :classrooms
resources :notifications
end
end
end
registrations_controller.rb (API contorller)
class Api::V1::RegistrationsController < Devise::RegistrationsController
respond_to :json
def create
if params[:email].nil?
render :status => 400,
:json => {:message => 'User request must contain the user email.'}
return
elsif params[:password].nil?
render :status => 400,
:json => {:message => 'User request must contain the user password.'}
return
end
if params[:email]
duplicate_user = User.find_by_email(params[:email])
unless duplicate_user.nil?
render :status => 409,
:json => {:message => 'Duplicate email. A user already exists with that email address.'}
return
end
end
#user = User.create(user_params)
if #user.save!
render :json => {:user => #user}
else
render :status => 400,
:json => {:message => #user.errors.full_messages}
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute, :first_name, :last_name, :access_code])
end
end
End Point for registration
http://localhost:3000/api/v1/users
Sample Postman response
{
"message": [
"Email can't be blank",
"Password can't be blank",
"Access code is invalid [Beta]."
]
}
Any help would greatly be appreciated, as I am keen on learning more (and getting this to work!).
UPDATE 1
Here is what I get on the server after making a post request to generate a user...
Started POST "/api/v1/users" for 127.0.0.1 at 2017-02-22 09:22:11 -0800
Processing by Api::V1::RegistrationsController#create as */*
Parameters: {"user"=>{"email"=>"user#sampleapi.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "access_code"=>"uiux"}}
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."email" IS NULL LIMIT $1 [["LIMIT", 1]]
Completed 400 Bad Request in 2ms (Views: 0.2ms | ActiveRecord: 0.4ms)
Updated Registrations_controller
class Api::V1::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
respond_to :json
def create
#user = build_resource(sign_up_params)
if #user.persisted?
# We know that the user has been persisted to the database, so now we can create our empty profile
if resource.active_for_authentication?
sign_up(:user, #user)
render :json => {:user => #user}
else
expire_data_after_sign_in!
render :json => {:message => 'signed_up_but_#{#user.inactive_message}'}
end
else
if params[:user][:email].nil?
render :status => 400,
:json => {:message => 'User request must contain the user email.'}
return
elsif params[:user][:password].nil?
render :status => 400,
:json => {:message => 'User request must contain the user password.'}
return
end
if params[:user][:email]
duplicate_user = User.find_by_email(params[:email])
unless duplicate_user.nil?
render :status => 409,
:json => {:message => 'Duplicate email. A user already exists with that email address.'}
return
end
end
render :status => 400,
:json => {:message => resource.errors.full_messages}
end
end
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute, :first_name, :last_name, :access_code])
end
end
I'm pretty sure my main issue at this point is the format of my params, so any push in the right direction for this would be great. I did find this post but am finding it a little difficult to follow in terms of what got their API to work...
Here is 2 solution, choose one you like.
Override devise_parameter_sanitizer:
class ApplicationController < ActionController::Base
protected
def devise_parameter_sanitizer
if resource_class == User
User::ParameterSanitizer.new(User, :user, params)
else
super # Use the default one
end
end
end
Override sign_up_params:
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
Why?
If you go deeping to Devise ParameterSanitizer, the resource_name will be :api_v1_user, not just :user because of your routes:
namespace :api do
namespace :v1 do
devise_for :users, controllers: { registrations: 'api/v1/registrations' }
end
end
Error resource_name will cause sign_up_params always return empty hash {}
Why don't you try something like this:
user = User.create(sign_up_params)
if user.save
render status: 200, json: #controller_blablabla.to_json
else
render :status => 400,
:json => {:message => #user.errors.full_messages}
end
or even better. You might use something like tiddle gem to make session more secure:
respond_to :json
def create
user = User.create(sign_up_params)
if user.save
token = Tiddle.create_and_return_token(user, request)
render json: user.as_json(authentication_token: token, email:
user.email), status: :created
return
else
warden.custom_failure!
render json: user.errors, status: :unprocessable_entity
end
end
You might use httpie --form to make the request:
http --form POST :3000/users/sign_up Accept:'application/vnd.sign_up.v1+json' user[email]='he#llo.com' user[username]='hello' user[password]='123456789' user[password_confirmation]='123456789'
do not forget:
def sign_up_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
end
I don't know what i'm missing, let me know if i'm wrong or something is wrong and i did't realize!
Regards!
Why not use the simple_token_authentication gem ?
Extremely simple to setup:
# Gemfile
gem "simple_token_authentication"
bundle install
rails g migration AddTokenToUsers "authentication_token:string{30}:uniq"
rails db:migrate
# app/models/user.rb
class User < ApplicationRecord
acts_as_token_authenticatable
# [...]
end
In your routes:
# config/routes.rb
Rails.application.routes.draw do
# [...]
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :classrooms
resources :notifications
end
end
end
In your controllers:
# app/controllers/api/v1/classrooms_controller.rb
class Api::V1::ClassroomsController < Api::V1::BaseController
acts_as_token_authentication_handler_for User
# [...]
end
Example call using the RestClient gem:
url = "http://localhost:3000/api/v1/classrooms/"
params = {user_email: 'john#doe.com', user_token: '5yx-APbH2cmb11p69UiV'}
request = RestClient.get url, :params => params
For existing users who don't have a token:
user = User.find_by_email("john#doe.com")
user.save
user.reload.authentication_token
...So close to desired behavior!
Have a Devise / AngularJS / Rails 4 project where the user login/logout is working nearly perfectly. Sending back the X-CSRF-Token and storing the user's email in angular as a cookie so that user hitting refresh doesn't cause angular to lose knowledge of the user.
Only problem I can see is this: If you are logged in as an admin user then, WITHOUT logging out... You just fill in the username/login with some other valid credentials... Devise is authenticating you based on your CSRF token instead of actually validating that username and password.
Therefore, you'll think you are logged in as someone else, but you are actually the previously-logged-in user. I'm sure this is pretty simple, but just can't seem to nail it... need to force devise/warden to always check username/password on the CREATE - don't just be happy with the csrf token.
Here is our sessions_controller.rb:
class SessionsController < Devise::SessionsController
respond_to :json
def create
Rails.logger.debug("(SessionsController.create) ******* ")
user = warden.authenticate!(:scope => :user, :recall => "#{controller_path}#failure")
Rails.logger.debug("(SessionsController.create) back from warden.authenticate ")
render :status => 200,
:json => { :success => true,
:info => "Logged in",
:user => current_user
}
end
def destroy
warden.authenticate!(:scope => :user, :recall => "#{controller_path}#failure")
sign_out
render :status => 200,
:json => { :success => true,
:info => "Logged out",
}
end
def failure
render :status => 401,
:json => { :success => false,
:info => "Login Credentials Failed"
}
end
def show_current_user
warden.authenticate!(:scope => :user, :recall => "#{controller_path}#failure")
render :status => 200,
:json => { :success => true,
:info => "Current User",
:user => current_user
}
end
end
Here is application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery #with: :exception
helper_method :require_user, :require_admin
after_filter :set_csrf_cookie_for_ng
def set_csrf_cookie_for_ng
Rails.logger.debug("set_csrf_cookie_for_ng called FAT:#{form_authenticity_token}")
Rails.logger.debug("protect_against_forgery = #{protect_against_forgery?}")
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
def require_user
Rails.logger.debug("require_user method in application controller")
if !user_signed_in?
flash[:notice] = "Please sign in!"
redirect_to new_user_session_path
end
end
def is_admin_user
return false if current_user.nil?
return current_user.admin?
end
def is_any_user
return false if current_user.nil?
return user_signed_in?
end
def verify_admin_user
unless is_admin_user
Rails.logger.debug("Unauthorized - Must be Admin")
respond_to do | format |
format.json { render :json => [], :status => :unauthorized }
end
end
end
def verify_any_user
unless user_signed_in?
Rails.logger.debug("Unauthorized - Must be Logged-in")
respond_to do | format |
format.json { render :json => [], :status => :unauthorized }
end
end
end
protected
def verified_request?
Rails.logger.debug("verified_request called f_a_t:#{form_authenticity_token}. X-XSRF-TOKEN:#{request.headers['X-XSRF-TOKEN']}")
super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
end
end
Routes.rb:
devise_for :users, :controllers => {:sessions => "sessions"}
Finally, on the Angularjs side, our httpprovider:
.config(["$httpProvider", ($httpProvider) ->
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content")
Here is what I see going by in the logs if you were logged-in as an admin, then you attempt to "login-over" as a non admin... Note that "user 1" is the admin who was logged-in already.
Started POST "/users/sign_in.json" for 127.0.0.1 at 2014-06-02 23:21:25 -0500
Processing by SessionsController#create as JSON
Parameters: {"user"=>{"email"=>"nonadmin#example.com", "password"=>"[FILTERED]"}, "session"=>{"user"=>{"email"=>"nonadmin#example.com", "password"=>"[FILTERED]"}}}
verified_request called f_a_t:P2FZxg/qwooeNp0Ttme8urXoM9tzWh1bO5y28J8rTHc=. X-XSRF-TOKEN:P2FZxg/qwooeNp0Ttme8urXoM9tzWh1bO5y28J8rTHc=
(SessionsController.create) *******
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
(SessionsController.create) back from warden.authenticate
set_csrf_cookie_for_ng called FAT:P2FZxg/qwooeNp0Ttme8urXoM9tzWh1bO5y28J8rTHc=
protect_against_forgery = true
Completed 200 OK in 3ms (Views: 0.8ms | ActiveRecord: 0.5ms)
Appreciate any help!
Devise/warden (by default) uses a session/cookie based authentication, so its not the CSRF token thats authenticating its the session.
You have a couple of options going forward
1, Disable the login form when angular knows the user is logged in
2, Explicitly sign the user out (by calling sign_out) in your create action
3, (Preferred) Don't use session authentication over an api. An API should be stateless. By that I mean that the server shouldn't know about whether a user is 'logged in'. You should be sending an authentication token with every request (not relying on the session). Look here http://www.soryy.com/ruby/api/rails/authentication/2014/03/16/apis-with-devise.html for a nice write up.
The basics of token based authentication are:
Angular sends username and password to server
Server authenticates username and password, generates a 'token' for that user and sends the token back to angular
Angular stores that token (in local storage / cookie)
With each new request (eg: /api/private_thing.json) Angular sends the 'token' (in a header or parameter)
Server checks that the token belongs to a user record with permission to view the 'private_thing'
Good luck
UPDATE
if you ARE going to go with option 2 then change your create action to:
def create
sign_out if is_any_user
render :status => 200, json: { success: true, info: "Logged in", user: current_user }
end
I am completely new to rails (actually this is my day 1 of rails). I am trying to develop a backend for my iOS app. Here is my create user method.
def create
user = User.find_by_email(params[:user][:email])
if user
render :json => {:success => 'false', :message => 'Email already exists'}
else
user = User.new(user_params)
if user.save
render :json => {:success => 'true', :message => 'Account has been created'}
else
render :json => {:success => 'false', :message => 'Error creating account'}
end
end
end
How can I make it better?
You could use HTTP status code, but it might be overkill if your API is not going to be used by anything but your iOS app.
The way I would do it is to put the validation on the model's side and let ActiveModel populate the errors. Status codes are also super useful.
class User < ApplicationModel
validate_uniqueness_of :email
# Other useful code
end
class UsersController < ApplicationController
def create
#user = User.new(params.require(:user).permit(:email)) # `require` and `permit` is highly recommended to treat params
if #user.save # `User#save` will use the validation set in the model. It will return a boolean and if there are errors, the `errors` attributes will be populated
render json: #user, status: :ok # It's good practice to return the created object
else
render json: #user.errors, status: :unprocessable_entity # you'll have your validation errors as an array
end
end
end
I have an issues with connecting loose end in oauth and authlogic.
I'm running rails 3.0.9 with authlogic working fine and I wanted to add on twitter login.
The issue that I'm having is that after logging in on twitter instead being redirected to call back url defined in twitter dev settings. The app redirects to top domain while appending this to the url
user_sessions?oauth_token=[t_o_k_e_n]
I don't have index action in user_sessions_controller.rb, so I get the action index couldn't be found error, but I can't decipher why I'm being redirected to this url?
My user_sessions.rb
class UserSession < Authlogic::Session::Base
# def to_key
# new_record? ? nil : [ self.send(self.class.primary_key) ]
# end
#
# def persisted?
# false
# end
#
def self.oauth_consumer
OAuth::Consumer.new("asdasdsad", "asdasdasdas",
{ :site=>"http://twitter.com",
:authorize_url => "http://twitter.com/oauth/authenticate"})
end
end
My user_sessions_controller.rb
class UserSessionsController < ApplicationController
# GET /user_sessions/new
# GET /user_sessions/new.xml
def new
#user_session = UserSession.new
end
# POST /user_sessions
# POST /user_sessions.xml
def create
#user_session = UserSession.new(params[:user_session])
#user_session.save do |result|
if result
flash[:notice] = "Login successful!"
redirect_back_or_default root_path
else
render :action => :new
end
end
# respond_to do |format|
# if #user_session.save
# format.html { redirect_to(root_path, :notice => 'User session was successfully created.') }
# format.xml { render :xml => #user_session, :status => :created, :location => #user_session }
# else
# format.html { render :action => "new" }
# format.xml { render :xml => #user_session.errors, :status => :unprocessable_entity }
# end
# end
end
# DELETE /user_sessions/1
# DELETE /user_sessions/1.xml
def destroy
#user_session = UserSession.find
#user_session.destroy
respond_to do |format|
format.html { redirect_to(root_path, :notice => 'Goodbye!') }
format.xml { head :ok }
end
end
end
I even tried adding
:oauth_callback => "http://127.0.0.1:3000/"
to the Consumer.new clause, but that didn't help.
Lastly, my routes.rb looks like this:
resources :users, :user_sessions
match 'login' => 'user_sessions#new', :as => :login
match 'logout' => 'user_sessions#destroy', :as => :logout
Anyone has any ideas on how to troubleshoot this or had a similar problem?
https://dev.twitter.com/sites/default/files/images_documentation/oauth_diagram.png defines quite clearly what you should send and get from Twitters Oauth Provider.
Are you sure you get a oauth_callback_confirmed in step B, if so you might wanna contact Twitter why they validate your oauth_callback then modify it
Working from the railscast #160 base code I've set up a very simple site that allows me to log in, out and register an account. (Its almost identical except that I've removed the 'username' from the users migrate table and relevant views so only an email address is required)
I'm now trying to create a new log in action so that I can log in via JSON.
I'd like to be able to send a get request to http://app:3000/apilogin?email=my#email.com&password=p4ssw0rd and have the rails app store the IP address the request came from (if the log in was correct) and send a relevant response (in JSON).
So far I have added a section to controllers/user_sessions_controller.rb so that it goes:
class UserSessionsController < ApplicationController
#...
def new_api
respond_to do |format|
format.json
end
end
end
To routes.rb:
map.apilogin "apilogin", :controller => "user_sessions", :action => "new_api"
But I'm at a loss as to what to put in views/user_sessions/new_api.json.rb! Can you help?
You don't need to define a view at all - just return appropriate json from the controller.
def new_api
#user_session = UserSession.new({:email => params[:email], :password => params[:password]})
respond_to do |format|
if #user_session.save
format.json { render :json => {:success => true} }
else
format.json { render :json => {:success => false, :message => 'incorrect username or password'}, :status => :unauthorized }
end
end
end
You can do something like this:
def new_api
respond_to do |format|
format.json { render :json => user.slice(:name).to_json }
end
end
Or you can also generate the JSON in views/user_sessions/new_api.json.erb as you would write normal erb code. Not a good idea though:
{"name":"<%= #user.name %>"}