waiting-for-dev / devise-jwt

JWT token authentication with devise and rails

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Not Authorizing on custom devise signup route

maqsudinamdar opened this issue · comments

Problem Statement

I'm Newbie in Rails and following this tutorial for setting up JWT based authentication in API.

This rails project works fine for a web applications. However, Incase of API I'm getting empty resource while I still have value in params.

Expected behavior

Return Authorization token after signup

Environment

rails (6.1.4)

devise (4.8.0)
devise-jwt (0.9.0)

warden (1.2.9)
warden-jwt_auth (0.6.0)

Controller & Route

app/controllers/api/v1/users/registrations_controller.rb

class Api::V1::Users::RegistrationsController < Devise::RegistrationsController
    respond_to :json
    skip_before_action :verify_authenticity_token
  
    # POST /resource
    def create
      super
    end
  
    private
    def respond_with(resource, _opts = {})
    if resource.persisted?
        render json: {
        status: { code: 200, message: "Signed up sucessfully." },
        data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
        }
    else
        render json: {
        status: { message: "User couldn't be created successfully. #{resource.errors.full_messages.to_sentence}" }
        }, status: :unprocessable_entity
    end
    end
end

config/routes.rb

namespace :api do
    namespace :v1 do

      devise_for :users, path: '', path_names: {
        sign_in: 'login',
        sign_out: 'logout',
        registration: 'signup'
      },
      controllers: {
        sessions: 'api/v1/users/sessions',
        registrations: 'api/v1/users/registrations'
      }
    end
end

Debugging information

|    66:   private
|    67:     def respond_with(resource, _opts = {})
|    68:       byebug
| => 69:       if resource.persisted?
|    70:         render json: {
|    71:           status: { code: 200, message: "Signed up sucessfully." },
|    72:           data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
|    73:         }
| (byebug) resource
| #<User 
    id: nil, 
    email: "", 
    first_name: "", 
    last_name: "", 
    role: "member", 
    created_at: nil, 
    updated_at: nil, 
    jti: nil
>

| (byebug) params
| #<ActionController::Parameters 
    {
        "email"=>"test@test.com", 
        "first_name"=>"John", 
        "last_name"=>"Wick", 
        "password"=>"password", 
        "controller"=>"api/v1/users/registrations", 
        "action"=>"create", 
        "registration"=>{
            "email"=>"test@test.com", 
            "first_name"=>"John", 
            "last_name"=>"Wick", 
            "password"=>"password"
        }
    } permitted: false>

Setup and configuration

  • Output of Devise::JWT.config
Devise::JWT.config
 => #<Dry::Configurable::Config 
 values={
    :secret=>"secret", 
    :expiration_time=>1800, 
    :dispatch_requests=>[
        ["POST", /^\/api\/v1\/login$/], 
        ["POST", /^\/users\/sign_in$/], 
        ["POST", /^\/users$/], 
        ["POST", /^\/api\/v1\/login$/], 
        ["POST", /^\/api\/v1\/signup$/]
    ], 
    :revocation_requests=>[
        ["DELETE", /^\/api\/v1\/logout$/], 
        ["DELETE", /^\/users\/sign_out$/], 
        ["DELETE", /^\/api\/v1\/logout$/]
    ], 
    :aud_header=>"JWT_AUD", 
    :request_formats=>{}
}> 

  • Output of Warden::JWTAuth.config
Warden::JWTAuth.config
 => #<Dry::Configurable::Config 
 values={
    :secret=>"secret", 
    :algorithm=>"HS256", 
    :expiration_time=>1800, 
    :aud_header=>"JWT_AUD", 
    :mappings=>{
        :user=>User (call 'User.connection' to establish a connection), 
        :api_v1_user=>User (call 'User.connection' to establish a connection)}, 
        :dispatch_requests=>[
            ["POST", /^\/api\/v1\/login$/], 
            ["POST", /^\/users\/sign_in$/], 
            ["POST", /^\/users$/], 
            ["POST", /^\/api\/v1\/login$/], 
            ["POST", /^\/api\/v1\/signup$/]
        ], 
        :revocation_requests=>[
            ["DELETE", /^\/api\/v1\/logout$/], 
            ["DELETE", /^\/users\/sign_out$/], 
            ["DELETE", /^\/api\/v1\/logout$/]
        ], 
        :revocation_strategies=>{
            :user=>User (call 'User.connection' to establish a connection), 
            :api_v1_user=>User (call 'User.connection' to establish a connection)
        }
}>

  • Output of Devise.mappings
Devise.mappings => {
    :user=>#<Devise::Mapping:0x000055a4187118f0 
        @scoped_path="users", 
        @singular=:user, 
        @class_name="User", 
        @klass=#<Devise::Getter:0x000055a418711558 @name="User">, 
        @path="users", 
        @path_prefix=nil, 
        @sign_out_via=:delete, 
        @format=nil, 
        @router_name=nil, 
        @failure_app=Devise::FailureApp, 
        @controllers={
            :registrations=>"registrations", 
            :sessions=>"devise/sessions", 
            :passwords=>"devise/passwords"
        }, 
        @path_names={
            :registration=>"", 
            :new=>"new", 
            :edit=>"edit", 
            :sign_in=>"sign_in", 
            :sign_out=>"sign_out", 
            :password=>"password", 
            :sign_up=>"sign_up", 
            :cancel=>"cancel"
        }, 
        @modules=[
            :database_authenticatable, 
            :rememberable, 
            :recoverable, 
            :registerable, 
            :validatable, 
            :trackable, 
            :jwt_authenticatable
        ], 
        @routes=[:session, :password, :registration ], 
        @used_routes=[:session, :password, :registration ], 
        @used_helpers=[:session, :password, :registration ]
    >, 
    :api_v1_user=>#<Devise::Mapping:0x000055a418563620 
        @scoped_path="api_v1/users", 
        @singular=:api_v1_user, 
        @class_name="User", 
        @klass=#<Devise::Getter:0x000055a4185630a8 @name="User">, 
        @path="", 
        @path_prefix="/api/v1", 
        @sign_out_via=:delete, 
        @format=nil, 
        @router_name=nil, 
        @failure_app=Devise::FailureApp, 
        @controllers={
            :sessions=>"api/v1/users/sessions", 
            :registrations=>"api/v1/users/registrations", 
            :passwords=>"api/v1/passwords"}, 
            @path_names={
                :registration=>"signup", 
                :new=>"new", 
                :edit=>"edit", 
                :sign_in=>"login", 
                :sign_out=>"logout", 
                :password=>"password", 
                :sign_up=>"sign_up", 
                :cancel=>"cancel"
            }, 
            @modules=[
                :database_authenticatable, 
                :rememberable, 
                :recoverable, 
                :registerable, 
                :validatable, 
                :trackable, 
                :jwt_authenticatable
            ], 
            @routes=[:session, :password, :registration], 
            @used_routes=[:session, :password, :registration], 
            @used_helpers=[:session, :password, :registration]
    >
}
  • Request
curl -X POST \
http://127.0.0.1:3000/api/v1/signup \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{
    "email": "test@test.com",
    "first_name": "John",
    "last_name": "Wick",
    "password": "password"
}'
  • Response
{
    "status": {
        "message": "User couldn't be created successfully. Email can't be blank and Password can't be blank"
    }
}

It doesn't seem an issue with devise-jwt. Your user is not being created, and that's a responsibility that doesn't belong to this library but to devise itself. At first sight, I think you have a problem with your params, as they should be within the name of the user scope, like in `{"user": {"email": "...",...}}"

Hi @waiting-for-dev,

Thanks for your response. I tried keeping parameter inside "user" but then to it was not working.

  • Request
curl -X POST \
http://127.0.0.1:3000/api/v1/signup \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{  
    "user": {
        "email": "test@test2.com",
        "password": "password",
        "password_confirmation": "password"
    }
}'

  • Response
{
    "status": {
        "message": "User couldn't be created successfully. Email can't be blank and Password can't be blank"
    }
}

I'm really sorry for not being able to give more support here. You'd better be off trying to find a solution in stack overflow as it seems it's a problem with your setup (devise-jwt is not involved here).

I dig around devise-jwt, warden-jwt_auth, warden and devise codebases for hours to figure how to get a token on the headers after registration. Hopefully it will save people time.

Your RegistrationsController has to look like this:

class RegistrationsController < Devise::RegistrationsController
  # Needed by devise to authenticate the user after registration and return the auth token
  prepend_before_action :allow_params_authentication!, only: :create

  respond_to :json

  # Sign in after sign up
  def sign_up(resource_name, resource)
    resource = warden.authenticate!
    sign_in(resource_name, resource)
  end

  # By default devise tries to build the resources with a session like this:
  # resource_class.new_with_session(hash, session)
  # but because we are on API mode we don't have a session so we need to build the user
  # without it
  def build_resource(hash = {})
    self.resource = resource_class.new(hash)
  end
end

And the routes looks like this

Rails.application.routes.draw do
  devise_for :users,
    path: '',
    path_names: {
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      sessions: 'sessions',
      registrations: 'registrations'
    }
end

@maqsudinamdar override resource_name method in registration controller. By default it takes your controller path as a resource. So it looks like api_v1_user. And it tried to fetch params with this key. But params contains another hash which called user for registration. So you need to override method to be like that:

def resource_name
:user
end