Authentication through an API that uses Devise

Big goal: Making a Rails web client for my team’s existent API.

The API is built in Rails. We already have a working iOS client built off of the API. I want to make the Rails client do basically the same as the iOS app.

Subgoal: Making user authentication work.

I’m currently using HTTParty to call the API’s login url (which the iOS app uses to log in). The API uses Devise for authentication. The api_key needs to be passed in for every call that the iOS app makes. I’m assuming the same can be said about my Rails app.

Here is the User class:

require 'httparty'

class User
  include HTTParty
  base_uri 'api.otherapp.com'
  default_params :output => 'json'
  format :json

  def initialize(email, password)
    @ response = HTTParty.get('/api/v1/tokens?format=json', :query => {:email => email, :password => password})
  end

  def response
    @response.inspect
  end

  def self.find_by_email(email)
    HTTParty.get('/api/v1/users/', :query => {:email => email})
  end
end

user = User.new("email@email.com", "password")
puts user.response

The current error:

/Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1532:in `addr_port': undefined method `+' for nil:NilClass (NoMethodError)
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1465:in `begin_transport'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1410:in `transport_request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1384:in `request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1377:in `block in request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:851:in `start'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1374:in `request'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty/request.rb:93:in `perform'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty.rb:463:in `perform_request'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty.rb:399:in `get'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty.rb:496:in `get'
from user.rb:23:in `find_by_email'
from user.rb:51:in `<main>'

What I’ve tried:

requiring ‘net/http’ in User class
searching all over stackoverflow- nothing relevant yet.

Other info:

This app does not use ActiveRecord or models because all the data will be retrieved (and sent to) the API.

Quick Answer:

Change HTTParty.get to self.class.get.

Long Answer:

You’re seeing this error because HTTParty.get doesn’t know which host you want to connect to. It’s parsing /api/v1/users/ as a fully-formed URL and getting a nil host. Ruby’s net/http is crashing hard when it attempts to connect to nil.

HTTParty defines class methods for HTTP methods like get and post when you include it. To use the class configuration options like base_uri, you’ll need to call those methods like self.class.get instead of HTTParty.get.

If you call HTTParty.get directly, it doesn’t know about anything configured for your User class, so you need to provide a fully-formed URL and include options like format in the method call.

1 Like

Thanks @jferris! That makes sense, and resolved the error. Before, I had to put the base URI into the path manually.

UPDATE: First error has been resolved.

Two questions:

(1) An issue with calling HTTParty methods like GET:

user.rb:11:in `initialize': undefined method `get' for #<User:0x007fdd8cbb3a48> (NoMethodError)

I can temporarily resolve the first issue by replacing initialize with self.new, but hopefully there is a more elegant solution once I have a deeper understanding of how class initialization works under the hood (because on the superficial level, initialize behaves like a class method, like self.othermethod).

(2) Error message RE nil variable in http_version method

/Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1470:in `end_transport': undefined method `http_version' for nil:NilClass (NoMethodError)
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1441:in `transport_request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1384:in `request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1377:in `block in request'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:851:in `start'
from /Users/janet/.rvm/rubies/ruby-2.0.0-rc1/lib/ruby/2.0.0/net/http.rb:1374:in `request'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty/request.rb:93:in `perform'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty.rb:463:in `perform_request'
from /Users/janet/.rvm/gems/ruby-2.0.0-rc1/gems/httparty-0.11.0/lib/httparty.rb:399:in `get'
from user.rb:14:in `login'
from user.rb:53:in `<main>'

It appears that, from the beginning of processing the GET request, HTTParty’s res variable is set to nil for some reason.

Can you paste or link the code as it is now?

require 'httparty'

class User
  include HTTParty
  base_uri 'localhost:3000'
  default_params :format => 'json'
  format :json

  def initialize
  end

  def self.login(email, password)
    @response = post('/api/v1/tokens', :query => {:email => email, :password => password})
  end

  def response
    @response.inspect
  end

  def self.find_by_email(email)
    @response = get('/api/v1/users/activity', :query => {:email => email})
  end
end

user = User.login("email@fakeemail.com", "password")
puts user.response

Thanks. I’m a little confused, here; are you still getting the NoMethodError when calling get on a User instance?

The way new and initialize work in Ruby are a little confusing at first. Here’s the basic workflow:

  • You define a new class, like User.
  • You define an initialize instance method on that class to set up initial instance state.
  • You call User.new to instantiate a new instance of User, which is a class method.
  • Ruby’s default implementation of new allocates an instance of the class and then calls your initialize method on the new instance.

You generally do not override new or call initialize directly. Does that make sense?

I’m having a hard time understanding the backtrace with the http_version error. The lines referenced in user.rb from the backtrace don’t seem to match up with the latest user.rb code you pasted. Can you paste a backtrace along with the same code that produced it?

Error (2) from above has been resolved! It was an error from changes made in the net/http code while debugging the previous error.

Thanks @jferris for the explanation of class initialization. We ended up calling User.get(foo) instead of self.get(foo), which solved the initialization problem.

Now that we have worked through these smaller issues, a bigger picture question:

What are ways to do sessions management in the client app without writing a system from scratch? Are gems like Devise or Clearance necessary when there is already a sessions management/authentication system in the API? (Our API uses Devise, but we aren’t married to using that in the client app).

Or, is there a way to manage sessions outside of the client app?

No Devise isn’t needed on the client app. Essentially, when a user is authenticated on the API, that successful authentication should send a message to the Client (in my personal case, it’s an auth_hash that contains the relevant information for a User. You would need a Sessions Controller in the Client app to both sent the request to the Provider App (upon initial login) and then to receive the authentication hash from the Provider. Then in that Client Sessions Controller, you can then do whatever you need to do, provided that you receive that message from the provider app.

This actually seems like a great use case for OmniAuth. You’d need to set your Provider (API) up as an Omniauth Provider and then Omniauth would handle sending the auth hash upon successful login to the client. With the project I’m working on, I use Redis to manage sessions for 3 Clients and 1 Provider. I have all the apps sharing one Redis store, so each app can easily send stuff back and forth so App 1 knows when the user is logged in and then App 3 would already know that the user already has an active session on App 1, thus persisting a single session across all apps. This was a solution I just came up with, so it might not be the “Best” way, but it has been working for us.

1 Like