0
votes

I am using the Omniauth gem paired with the Devise gem for user authentication (here is a wiki. I chose Oauth2 strategy for authenticating Instagram user in my app.

My issue is that Users signing in through Instagram authentication are not persisted to my User model. After authenticating with Instagram they are redirected to a new user registration path localhost:3000/users/sign_up.

controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def instagram
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])
    if @user.persisted?

      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Instagram") if is_navigational_format?
    else
      session["devise.instagram_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  devise :omniauthable, :omniauth_providers => [:instagram]

  def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0,20]
    # user.name = auth.info.name   # assuming the user model has a name
    # user.image = auth.info.image # assuming the user model has an image
  end
end

def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.instagram_data"] && session["devise.instagram_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
      end
    end
end

end

Users table has the appropriate column (provider, uid)

  create_table "users", force: :cascade do |t|
    t.string   "email",                  default: "", null: false
    t.string   "encrypted_password",     default: "", null: false
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,  null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "provider"
    t.string   "uid"
  end

Finally, here is the output when signing in via Instagram authentication

Started GET "/users/auth/instagram" for ::1 at 2015-09-25 09:57:54 -0400
I, [2015-09-25T09:57:54.577637 #7451]  INFO -- omniauth: (instagram) Request phase initiated.


Started GET "/users/auth/instagram" for ::1 at 2015-09-25 09:57:54 -0400
I, [2015-09-25T09:57:54.655615 #7451]  INFO -- omniauth: (instagram) Request phase initiated.


Started GET "/users/auth/instagram/callback?code=SOME_CODE&state=SOME_STATE" for ::1 at 2015-09-25 09:57:54 -0400
I, [2015-09-25T09:57:54.811861 #7451]  INFO -- omniauth: (instagram) Callback phase initiated.
Processing by Users::OmniauthCallbacksController#instagram as HTML
  Parameters: {"code"=>"SOME_CODE", "state"=>"SOME_STATE"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."provider" = ? AND "users"."uid" = ?  ORDER BY "users"."id" ASC LIMIT 1  [["provider", "instagram"], ["uid", "343664764"]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
Redirected to http://localhost:3000/users/sign_up
1

1 Answers

1
votes

After looking deeply into the code, adding debugger breakpoints to track to flow of data from view to controller to model and back to controller, I realized that my problem was that Instagram API does not provide the email of a particular user. Since Devise requires an email by default and I was assigning a blank value to email user.email = auth.info.email the saving never completed and I would be redirected to the new user registration path new_user_registration_url.

How did I debug

First of, I needed an error message to actually see what was the problem. So, I added a "!" after save! in the from_omniauth class method.

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0,20]
    user.save! 
  end
end

With save!, validations always run and if i get any fails, an ActiveRecord::ActiveRecordError gets raised. The error message then stated that the email field cannot be blank.

So Now I actually had more information to work with. My workaround isn't great and I wouldn't suggest it-- but it solved the problem.

The workaround

My solution was to create a dummy email for each user who signs up via Instagram. In my from_omniauth method I assigned the user email field to a concatenation of the nickname (returned from an instagram authentication) field and a generic "@example.com":

user.email = auth.info.nickname + "@example.com"

This is not a sustainable solution because you are going to end up having the wrong email for every person who signups with Instagram (forget sending emails to them through Action Mailer).

A better solution that I haven't implemented yet (but will) is to redirect a user who signup/in through Instagram to a specific URL where you will prompt the user to enter their email address. You can then send that info back so it can be handled by the controller and saved correctly.

Of course that's an extra step that the User might not be willing to make however email addresses are a valuable asset and can be used to do many things like newsletters and other promotional emailing.