Setting up an API using Devise for authentication
Asked Answered
B

3

6

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"=>"[email protected]", "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...

Brendonbrenk answered 22/2, 2017 at 5:15 Comment(7)
how did you pass params?Jonson
I passed them through Postman as part of the post request body as follows: email: [email protected] password: password access_code: uiuxKelm
please pass it as users[email] ,users[password]Jonson
That still didn't seem to work...I get the same error as I pasted in the updated post above,Kelm
can you show me logs?Jonson
Can you clarify which logs exactly? Are you talking about my development log file? Or server output in the terminal? If you mean the latter, I updated the post already to contain the new error.Kelm
got it working ?Moderation
H
6

Here is 2 solution, choose one you like.

  1. 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
    
  2. 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 {}

Haslet answered 17/8, 2017 at 9:37 Comment(1)
Hi @joseph, you don't need to override the methods. The solution to create expected resource_name is to pass singular option to devise_for. Ref: https://mcmap.net/q/1776324/-devise-jwt-quot-you-need-to-sign-in-or-sign-up-before-continuing-quot.Englis
T
1

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]='[email protected]' 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!

Transmontane answered 22/2, 2018 at 21:11 Comment(0)
A
0

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: '[email protected]', user_token: '5yx-APbH2cmb11p69UiV'}

request = RestClient.get url, :params => params

For existing users who don't have a token:

user = User.find_by_email("[email protected]")
user.save
user.reload.authentication_token
Aweigh answered 22/2, 2017 at 9:39 Comment(1)
I've already got JWT's working for general authentication (such as signing in), so I'd like to stick with it rather than pull that part out of the app since it's already integrated.Kelm

© 2022 - 2024 — McMap. All rights reserved.