Showing posts with label rails-api. Show all posts
Showing posts with label rails-api. Show all posts

Wednesday, October 17, 2018

Rails Devise API - Login route responds with `You need to sign in or sign up before continuing.`

1 comment

I'm currently using Devise with my Rails API app to authenticate users using devise-jwt.

This is what my User model looks like:

class User < ApplicationRecord   devise :database_authenticatable,          :registerable,          :jwt_authenticatable,          jwt_revocation_strategy: JWTBlackList end 

And config/routes.rb is set up 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 

This is the sessions controller:

class SessionsController < Devise::SessionsController    private    def respond_with(resource, _opts = {})     render json: resource   end    def response_to_on_destroy     head :no_content   end end 

and the registrations controller:

class RegistrationsController < Devise::RegistrationsController   respond_to :json    def create     build_resource(sign_up_params)      resource.save     render_resource(resource)   end end 

I ran some Rspec tests shown below which were successful -

require 'rails_helper'  RSpec.describe SessionsController, type: :request do   let(:user) { create(:user) }   let(:url) { '/login' }   let(:params) do     {       user: {         email: user.email,         password: user.password       }     }   end    context 'when params are correct' do     before do       post url, params: params     end      it 'returns 200' do       expect(response).to have_http_status(200)     end      it 'returns JTW token in authorization header' do       expect(response.headers['Authorization']).to be_present     end      it 'returns valid JWT token' do       decoded_token = decoded_jwt_token_from_response(response)       expect(decoded_token.first['sub']).to be_present     end   end end 

But when I run the following POST request to /login on Postman I get the following message:

postman request

On the right hand side you are able to see the rails console and server, showing the credentials are correct, but still we get 401

Any clues on what might be wrong? It's been difficult finding good resources on Devise using Rails API.

Thank you in advance

1 Answers

Answers 1

After digging through the SessionsController I found the reason it returned 401: warden was trying to authenticate an empty body. Fixed this by added Content-Type to the request header

Read More

Friday, April 14, 2017

Advantages of using OAuth Password Credentials Grant over Token Access Authentication

Leave a Comment

I am creating an API for a mobile application using Rails 5. At the moment, I don't need three-legged authorization, as the API will only be consumed by our own mobile apps.

I'm thinking of choosing one of these two options:

  1. Doorkeeper + Devise: I can implement OAuth 2.0 using Doorkeeper, but I will only be using the Password Credentials Grant, at least for now.
  2. Rails' own ActionController::HttpAuthentication::Token module + Devise: This seems like the simpler way to go.

Honestly, I can't see the difference between the Token Access Authentication method and OAuth 2.0's Password Credentials Grant.

How would one choose one over the other? Is there any other option that needs to be considered?

2 Answers

Answers 1

If all you'll ever have is the "single purpose API" (only for mobile application), there is no big difference in terms of security.

But if you'd like to extend this API to be used by the external services, then, you'll be in a much better position with implemented OAuth 2.0 using Doorkeeper, because you can easily configure, for example, a Authorization Code Grant for them. So, I'd say that "Doorkeeper + Devise" option is more future-proof, because it provides more options, while HTTP Token authentication is much simpler for implementation.

Answers 2

The two are conceptually different things:

  • "Token Access Authentication" specifies a protocol describing how a (possibly long-lived) token should be presented to the server securely. It says nothing about where the token came from or how it should be interpreted.
  • "Password Credentials Grant" is a part of the fully-fledged OAuth flow, describing the means of obtaining a (usually short-lived) token.

In a sense, you could use Password Credentials Grant to obtain a token using OAuth and then use this token in the Token authorization header to gain access.

The question then becomes - is it useful to do the extra roundtrip for exchanging the (permanent and secret) credentials for a (temporary) token, when we could instead use the (permanent and secret) token stored in the app to authorize immediately anyway.

As I see it, there are two potential benefits of using the full OAuth flow. Firstly, it lets you add other means of authorization for third parties naturally. Secondly, it lets you revoke the token at any moment and force a re-authorization using these other means (if you implement them, of course) without having to invent any wheels.

On the other hand, you can always add any additional "token generation" parts later, when they are needed. Thus, if your current plan is to hard-code credentials in code anyway, I'd suspect you are probably better off relying on Token authorization.

Not only is it one request shorter, it may also be slightly more secure than the Bearer authentication used in OAuth: if an attacker sniffs a Bearer token, they would (usually) get full token access to the server until its expiration. It is not the case with Token tokens (normally). Not that it all matters too much of course if the attacker could extract the shared secret from your app anyway.

Read More

Thursday, April 21, 2016

Using Rails 5 (the API flag) and getting no route error despite route declared

Leave a Comment

This is my controller:

class Api::V1::UsersController < ApplicationController   respond_to :json    def show     respond_with User.find(params[:id])   end end 

This is my routes.rb

require 'api_constraints'  Rails.application.routes.draw do   devise_for :users   # Api definition   namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do     scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do       resources :users, :only => [:show]     end   end end 

This is my lib/api_constraints.rb

class ApiConstraints   def initialize(options)     @version = options[:version]     @default = options[:default]   end    def matches?(req)     @default || req.headers['Accept'].include?("application/vnd.myapp.v#{@version}")   end end 

I added a record to my DB as can be seen here:

[3] pry(main)> User.all   User Load (0.4ms)  SELECT "users".* FROM "users" => [#<User:0x007fbdb31079f8   id: 1,   email: "abc@test.com",   encrypted_password: "$2a$11$rvOrK1bmuuNwwc78ERxG3eCrKiUu9NTZsJ/nmirqb.3yRBHYUK69S",   reset_password_token: nil,   reset_password_sent_at: nil,   remember_created_at: nil,   sign_in_count: 0,   current_sign_in_at: nil,   last_sign_in_at: nil,   current_sign_in_ip: nil,   last_sign_in_ip: nil,   created_at: Mon, 11 Apr 2016 10:35:39 UTC +00:00,   updated_at: Mon, 11 Apr 2016 10:35:39 UTC +00:00>] 

Yet this is the error I get in my rails console:

ActionController::RoutingError (No route matches [GET] "/users/1"):  actionpack (5.0.0.beta3) lib/action_dispatch/middleware/debug_exceptions.rb:53:in `call' web-console (3.1.1) lib/web_console/middleware.rb:131:in `call_app' web-console (3.1.1) lib/web_console/middleware.rb:28:in `block in call' web-console (3.1.1) lib/web_console/middleware.rb:18:in `catch' web-console (3.1.1) lib/web_console/middleware.rb:18:in `call' actionpack (5.0.0.beta3) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call' railties (5.0.0.beta3) lib/rails/rack/logger.rb:36:in `call_app' railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `block in call' activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `block in tagged' activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:26:in `tagged' activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `tagged' railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `call' actionpack (5.0.0.beta3) lib/action_dispatch/middleware/request_id.rb:24:in `call' rack (2.0.0.alpha) lib/rack/runtime.rb:22:in `call' activesupport (5.0.0.beta3) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call' actionpack (5.0.0.beta3) lib/action_dispatch/middleware/load_interlock.rb:13:in `call' actionpack (5.0.0.beta3) lib/action_dispatch/middleware/static.rb:136:in `call' rack (2.0.0.alpha) lib/rack/sendfile.rb:111:in `call' railties (5.0.0.beta3) lib/rails/engine.rb:522:in `call' puma (3.2.0) lib/puma/configuration.rb:227:in `call' puma (3.2.0) lib/puma/server.rb:561:in `handle_request' puma (3.2.0) lib/puma/server.rb:406:in `process_client' puma (3.2.0) lib/puma/server.rb:271:in `block in run' puma (3.2.0) lib/puma/thread_pool.rb:111:in `block in spawn_thread' 

And this is what I see in my browser:

{"status":404,"error":"Not Found","exception":"#\u003cActionController::RoutingError: No route matches [GET] \"/users/1\"\u003e","traces":{"Application Trace":[],"Framework Trace":[{"id":0,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/debug_exceptions.rb:53:in `call'"},{"id":1,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:131:in `call_app'"},{"id":2,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:28:in `block in call'"},{"id":3,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:18:in `catch'"},{"id":4,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:18:in `call'"},{"id":5,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call'"},{"id":6,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:36:in `call_app'"},{"id":7,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `block in call'"},{"id":8,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `block in tagged'"},{"id":9,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:26:in `tagged'"},{"id":10,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `tagged'"},{"id":11,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `call'"},{"id":12,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/request_id.rb:24:in `call'"},{"id":13,"trace":"rack (2.0.0.alpha) lib/rack/runtime.rb:22:in `call'"},{"id":14,"trace":"activesupport (5.0.0.beta3) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'"},{"id":15,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/load_interlock.rb:13:in `call'"},{"id":16,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/static.rb:136:in `call'"},{"id":17,"trace":"rack (2.0.0.alpha) lib/rack/sendfile.rb:111:in `call'"},{"id":18,"trace":"railties (5.0.0.beta3) lib/rails/engine.rb:522:in `call'"},{"id":19,"trace":"puma (3.2.0) lib/puma/configuration.rb:227:in `call'"},{"id":20,"trace":"puma (3.2.0) lib/puma/server.rb:561:in `handle_request'"},{"id":21,"trace":"puma (3.2.0) lib/puma/server.rb:406:in `process_client'"},{"id":22,"trace":"puma (3.2.0) lib/puma/server.rb:271:in `block in run'"},{"id":23,"trace":"puma (3.2.0) lib/puma/thread_pool.rb:111:in `block in spawn_thread'"}],"Full Trace":[{"id":0,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/debug_exceptions.rb:53:in `call'"},{"id":1,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:131:in `call_app'"},{"id":2,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:28:in `block in call'"},{"id":3,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:18:in `catch'"},{"id":4,"trace":"web-console (3.1.1) lib/web_console/middleware.rb:18:in `call'"},{"id":5,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call'"},{"id":6,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:36:in `call_app'"},{"id":7,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `block in call'"},{"id":8,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `block in tagged'"},{"id":9,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:26:in `tagged'"},{"id":10,"trace":"activesupport (5.0.0.beta3) lib/active_support/tagged_logging.rb:70:in `tagged'"},{"id":11,"trace":"railties (5.0.0.beta3) lib/rails/rack/logger.rb:24:in `call'"},{"id":12,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/request_id.rb:24:in `call'"},{"id":13,"trace":"rack (2.0.0.alpha) lib/rack/runtime.rb:22:in `call'"},{"id":14,"trace":"activesupport (5.0.0.beta3) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'"},{"id":15,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/load_interlock.rb:13:in `call'"},{"id":16,"trace":"actionpack (5.0.0.beta3) lib/action_dispatch/middleware/static.rb:136:in `call'"},{"id":17,"trace":"rack (2.0.0.alpha) lib/rack/sendfile.rb:111:in `call'"},{"id":18,"trace":"railties (5.0.0.beta3) lib/rails/engine.rb:522:in `call'"},{"id":19,"trace":"puma (3.2.0) lib/puma/configuration.rb:227:in `call'"},{"id":20,"trace":"puma (3.2.0) lib/puma/server.rb:561:in `handle_request'"},{"id":21,"trace":"puma (3.2.0) lib/puma/server.rb:406:in `process_client'"},{"id":22,"trace":"puma (3.2.0) lib/puma/server.rb:271:in `block in run'"},{"id":23,"trace":"puma (3.2.0) lib/puma/thread_pool.rb:111:in `block in spawn_thread'"}]}} 

Edit 1

This is my rake routes

$ rake routes [DEPRECATION] `last_comment` is deprecated.  Please use `last_description` instead. [DEPRECATION] `last_comment` is deprecated.  Please use `last_description` instead. [DEPRECATION] `last_comment` is deprecated.  Please use `last_description` instead.                   Prefix Verb   URI Pattern                    Controller#Action         new_user_session GET    /users/sign_in(.:format)       devise/sessions#new             user_session POST   /users/sign_in(.:format)       devise/sessions#create     destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy            user_password POST   /users/password(.:format)      devise/passwords#create        new_user_password GET    /users/password/new(.:format)  devise/passwords#new       edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit                          PATCH  /users/password(.:format)      devise/passwords#update                          PUT    /users/password(.:format)      devise/passwords#update cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel        user_registration POST   /users(.:format)               devise/registrations#create    new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new   edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit                          PATCH  /users(.:format)               devise/registrations#update                          PUT    /users(.:format)               devise/registrations#update                          DELETE /users(.:format)               devise/registrations#destroy                 api_user GET    /users/:id(.:format)           api/v1/users#show {:format=>:json, :subdomain=>"api"} 

Edit 2

This is my application controller:

class ApplicationController < ActionController::API end 

Edit 3

This is my users_controller_spec.rb test that actually passes:

require 'rails_helper'  describe Api::V1::UsersController do   before(:each) { request.headers['Accept'] = "application/vnd.myapp.v1" }    describe "GET #show" do     before(:each) do       @user = FactoryGirl.create :user       get :show, id: @user.id, format: :json     end      it "returns the information about a user on a hash" do       user_response = JSON.parse(response.body, symbolize_names: true)       expect(user_response[:email]).to eql @user.email     end      it { should respond_with 200 }   end  end 

This is the result:

$ rspec spec/controllers DEPRECATION WARNING: use_transactional_fixtures= is deprecated and will be removed from Rails 5.1 (use use_transactional_tests= instead). (called from <top (required)> at /myapp/spec/controllers/api/v1/users_controller_spec.rb:3) DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions.  Examples:  get :show, params: { id: 1 }, session: { user_id: 1 } process :update, method: :post, params: { id: 1 }  (called from block (3 levels) in <top (required)> at /myapp/spec/controllers/api/v1/users_controller_spec.rb:9) DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions.  Examples:  get :show, params: { id: 1 }, session: { user_id: 1 } process :update, method: :post, params: { id: 1 }  (called from block (3 levels) in <top (required)> at /myapp/spec/controllers/api/v1/users_controller_spec.rb:9) .DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions.  Examples:  get :show, params: { id: 1 }, session: { user_id: 1 } process :update, method: :post, params: { id: 1 }  (called from block (3 levels) in <top (required)> at /myapp/spec/controllers/api/v1/users_controller_spec.rb:9) DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions.  Examples:  get :show, params: { id: 1 }, session: { user_id: 1 } process :update, method: :post, params: { id: 1 }  (called from block (3 levels) in <top (required)> at /myapp/spec/controllers/api/v1/users_controller_spec.rb:9) .  Finished in 0.41411 seconds (files took 3.33 seconds to load) 2 examples, 0 failures 

1 Answers

Answers 1

The problem is that your route has a constraint requiring a subdomain of api. So http://api.lvh.me:3000/users/1.json will work, but not http://localhost:3000/users/1.json or http://lvh.me:3000/users/1.json.

You should change your Ajax call to use an api subdomain. You can also confirm this by writing a request test (not a controller test), and see that it gets a 200 when using an api subdomain, and a 404 otherwise.

Read More