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
Related
I have the following rails_spec test:
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
fixtures :users, :clients
include AuthHelper
before do
#request.headers["Content-Type"] = 'application/json'
#request.headers["Accept"] = 'application/json'
end
describe '#create' do
context 'for a user with a valid authorization token' do
context 'having admin privileges' do
before do
init_headers_for_admin
end
context 'submitted with a valid set of parameters' do
it 'should create a new user having the submitted parameters' do
post :create, params: {
:name => 'Roland Deschain',
:email => 'roland#foamfactorybrewing.com',
:role => 'user',
:username => 'roland'
}
expect(response).to have_http_status(:ok)
user = json
expect(user).to_not be_nil
expect(user.name).to eq('Roland Deschain')
expect(user.email).to eq('roland#foamfactorybrewing.com')
expect(user.role).to eq('user')
expect(user.username).to eq('roland')
expect(user.created_at).to eq(user.updated_at)
end
end
I also have the following controller definition:
def create
# get_required_fields.each { |field|
# unless params.has_key? field
# render :json => { :error => 'A ' + field.to_s + ' must be specified in order to create a new User'}, :status => :unprocessable_entity and return
# end
# }
username = params[:username]
unless User.find_by_username(params[:username]) == nil
render :json => { :error => I18n.t('error_username_conflict') }, :status => :conflict and return
end
unless User.find_by_email(params[:email]) == nil
render :json => { :error => I18n.t('error_email_conflict') }, :status => :conflict and return
end
end
However, when I run the test, the params variable is always an empty hash. If I uncomment the code that detects whether I have the required parameters, it always returns an UNPROCESSABLE_ENTITY, rather than the NO_CONTENT I would expect if a username, name, and email are present. I'm not quite sure why, but I can't seem to get it to pass actual data via the post method. I've been looking at this for a while, and trying to debug it, but I can't seem to get it to pass the parameters correctly.
I'm using Rails 5 and rspec_rails 3.8.1.
EDIT:
Something is really wrong here, because if I simplify create to be:
def create
unless params.has_key?('name') && params.has_key?('username') && params.has_key?('email')
render :json => {:error => I18n.t('error_must_specify_username_email_and_name')}, :status => :unprocessable_entity and return
end
end
And execute the same spec as before (obviously expecting it to return no_content, and not unprocessable_entity), I end up with the same thing happening - in debug mode, it looks like the params variable upon entering this method is:
{"controller"=>"users", "action"=>"create", "user"=>{}}
So, first off, it's setting the user parameter, which isn't exactly what I intended, and moreover, it's setting it to an empty hash. I'm not sure what's causing this. I feel like it's something I'm forgetting to do from Rails 5.0, because this pattern seems to work fine in Rails 4.0.
EDIT 2:
I modified the test to be:
it 'should create a new user record' do
post :create, params: { user: #user }, as: :json
expect(response).to have_http_status(:ok)
expect(json.name).to eq(#user.name)
expect(json.email).to eq(#user.email)
expect(json.username).to eq(#user.username)
expect(json.role).to eq(#user.role)
end
And modified the UsersController class to look like:
class UsersController < ApplicationController
include AuthHelper
before_action :validate_api_key_for_auth, only: :login
before_action :authenticate_as_admin, only: [:create]
def create
p user_params
unless user_params.has_key?('name') && user_params.has_key?('username') && user_params.has_key?('email')
render :json => {:error => I18n.t('error_must_specify_username_email_and_name')}, :status => :unprocessable_entity and return
end
end
def user_params
params[:user].permit([:username, :name, :email, :role]).to_h
end
end
It is printing out: {} for the users hash from user_params, but I don't understand why this is the case.
EDIT 3:
authenticate_as_admin is screwing up the parameters somehow. If I change the definition of authenticate_as_admin to true, it shows the correct hash and returns no_content as expected. The definition of the authenticate_as_admin function is below (defined in ApplicationController):
def authenticate_as_admin
authenticate_as :admin
end
def authenticate_as(role)
# First, get the headers for the request
authorization_header = get_authorization_header
unless authorization_header
render :json => {:error => I18n.t('error_must_be_logged_in')}, :status => :forbidden and return false
end
auth_token = authorization_header.split[1]
decoded_token = decode_authorization_header authorization_header
unless decoded_token
render :json => {:error => I18n.t('error_invalid_token')}, :status => :forbidden and return false
end
payload = decoded_token[0]
user_id = payload['user_id']
auth_token_expiration_date = payload['expiration_date']
#authenticated_user = User.find(user_id) rescue nil
unless #authenticated_user and #authenticated_user.auth_token == auth_token
render :json => {:error => I18n.t('error_must_be_logged_in')}, :status => :forbidden and return false
end
if auth_token_expiration_date < DateTime.now
render :json => {:error => I18n.t('error_auth_token_expired')}, :status => :forbidden and return false
end
unless #authenticated_user.role_at_least? role
render :json => {:error => I18n.t('error_insufficient_permission')}, :status => :forbidden and return false
end
true
end
I can't seem to see why this would muck up the params, though, especially considering this worked fine in Rails 4.2. Anyone see anything I'm missing?
EDIT 4:
I feel like I'm on the right track. It seems as though if the authenticate_as() function returns false, I get this behavior. If a before_action returns false, what should be the expected result of a controller action? I expected that the method called in the before_action would perform the appropriate render ... call, and the before_action would silently fail, never calling the function that would have been called. I'm still not quite sure why the function is failing, but I'm trying to understand the behavior that should happen. Perhaps authentication checking isn't a great use-case for before_actions in Rails 5?
Update Not valid for rails 5
Try this,
post :create, {
:name => 'Roland Deschain',
:email => 'roland#foamfactorybrewing.com',
:role => 'user',
:username => 'roland'
}
when you have the params: {} it gets sent as a nested hash, { params: {...} }
EDIT 1
Instead of passing params to post :create try stubbing out params call in controller, something like
allow(subject).to receive(:params).and_return(params)
and inside describe block,
let(:params) { ActionController::Parameters.new({ :name => '', :email => '' ... }) }
or just a regular hash if you don't need ActionController::Parameters
and call the :create without any parameters,
post :create
see if this helps, I am using this pattern in our specs in another project which is on route to rails 5.
I'm trying to use devise to authenticate a simple email/password login so users can access the rest of the API via auth tokens. I'm running into trouble with devise simply returning You need to sign in or sign up before continuing. Here's my code:
class LoginController < ApplicationController
respond_to :json
def login
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
render :status => 200,
:json => { :success => true,
:info => "Logged in",
:user => current_user
}
end
def failure
render :status => 401,
:json => { :success => false,
:info => "Login Failed",
:data => {} }
end
def resource_name
:user
end
def resource
#resource ||= User.new
end
def devise_mapping
#devise_mapping ||= Devise.mappings[:user]
end
end
routes.rb
devise_scope :user do
post 'register' => 'registration#create', :as => :registers
post 'login' => 'login#login', :as => :login
end
I'm sending the following post data:
{
"user" : {
"email" : "testPost4#fake.com",
"password" : "Password1"
}
}
Having browsed various posts I've added:
config.navigational_formats = [:json]
to the devise.rb file but it didn't solve the problem.
Adding skip_before_filter :authenticate_user! doesn't work either.
I wasn't able to get this working so I have reverted to the much simpler approach of checking and signing in manually.
def login
params = login_params
user = User.find_by_email(params['email'].downcase)
if user.valid_password?(params['password']) then
sign_in(user)
success
else
failure
end
end
...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 have a custom method outside the generic CRUD in my friendships controller called request. My problem is that I have before_filter :require_auth set to run before all methods in my FriendshipsController.
It was working fine except for the request method.
(This makes me think it has something to do with it being out of normal CRUD?)
When I call the request method now it skips the :require_auth and goes straight to the request method which is giving me errors as I define some variables in :require_auth that I need inside the request method.
Here is my FriendshipsController:
class FriendshipsController < ApplicationController
skip_before_filter :verify_authenticity_token, :only => [:create]
before_filter :require_auth
def create
#friendship = Friendship.new(user_id: params[:user_id], friend_id: params[:friend_id], status: params[:status])
if #friendship.save
render :status => 200, :json => {:message => "Friendship Created"}
else
render :status => 500, :json => { :message => "Problem creating friendship" }
end
end
def request
# friendID = params[:friend_id]
# userID = #currentuser.id
binding.pry
#userid = #currentuser.id
#friendid = params[:friend_id]
unless (#userid == #friendid || Friendship.exists?(user_id: #userid,friend_id: #friendid))
create(:user_id => userID, :friend_id => friendID, :status => 'pending')
create(:user_id => friendID, :friend_id => userID, :status => 'requested')
end
end
end
Here is my ApplicationController where I define require_auth:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
# protect_from_forgery with: :null_session
def require_auth
binding.pry
auth_token = request.headers["HTTP_AUTH_TOKEN"]
#user = User.find_by_auth_token(auth_token)
if #user.auth_token
#currentuser = #user
else
render :status => 401, :json => {:error => "Requires authorization"}
return false
end
end
end
Chris Peters was right in the comments. My problem was that rails already has request defined. I simple changed the method name to something else and it works.
Thanks.
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.