Knock: Using multiple authentication strategies

Created on 25 Sep 2015  Â·  11Comments  Â·  Source: nsarno/knock

How would you provide authentication using email/password and facebook login?

question

Most helpful comment

Hey, I created a sample application for facebook login + jwt. I'm not sure if its the best approach but it works: https://github.com/muZk/rails5-api-jwt-facebook-auth

It was designed for getting the FB accessToken "client" side (which could be a JS application or Mobile app).

@nsarno any thoughts?


The key files are:

facebook_user_token_controller.rb

class FacebookUserTokenController < ActionController::API

  before_action :authenticate

  def create
    render json: auth_token, status: :created
  end

  private

  def authenticate
    unless entity.present?
      raise Knock.not_found_exception_class
    end
  end

  def auth_token
    if entity.respond_to? :to_token_payload
      Knock::AuthToken.new payload: entity.to_token_payload
    else
      Knock::AuthToken.new payload: { sub: entity.id }
    end
  end

  def entity
    @entity ||=
      if FacebookService.valid_token?(auth_params[:access_token])
        data = FacebookService.fetch_data(auth_params[:access_token])
        User.find_or_create_by uid: data['id'] do |user|
          user.first_name = data['first_name']
          user.last_name = data['last_name']
          user.email = data['email']
        end
      end
  end

  def auth_params
    params.require(:auth).permit :access_token
  end

end

And facebook_service.rb which has the logic for facebook token validation and data retrieval:

class FacebookService

  def self.valid_token?(access_token)
    status = false
    begin
      # We need to check if the access_token is valid for our FB APP. Source: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#checktoken
      debug_token = Koala::Facebook::API.new(access_token).debug_token(app_access_token_info['access_token'])
      status = true if debug_token['data']['is_valid']
    ensure
      return status
    end
  end

  def self.fetch_data(access_token)
    Koala::Facebook::API.new(access_token).get_object('me', fields: 'name,first_name,last_name,email') if valid_token?(access_token)
  end

  def self.app_access_token_info
    @app_access_token ||= Koala::Facebook::OAuth.new.get_app_access_token_info
  end

end

All 11 comments

Could you give more details about what you are trying to achieve? Thanks

I want the user to have the options of logging in with an email/password or a social account (Facebook).

I store the social account info differently than the email/password.

Didn't look like there was a way to differentiate the type of credentials using knock.

By more details I meant are you using knock inside a rails API interacting with a single page javascript application? Something else? If you could try to describe more precisely what you already have and what you are missing I could give you better answers.

This is how you can customize how you retrieve the user when signing in:

inside config/initializers/knock.rb:

  ## Current user retrieval from handle when signing in
  ## --------------------------------------------------
  ##
  ## This is where you can configure how to retrieve the current user when
  ## signing in.
  ##
  ## Knock uses the `handle_attr` variable to retrieve the handle from the
  ## AuthTokenController parameters. It also uses the same variable to enforce
  ## permitted values in the controller.
  ##
  ## You must raise ActiveRecord::RecordNotFound if the resource cannot be retrieved.
  ##
  ## Default:
  # self.current_user_from_handle = -> (handle) { User.find_by! Knock.handle_attr => handle }

Note that this will be deprecated in future versions of Knock in favour of a method in the user model. But the general idea will stay the same.

Is this helping you? If not, don't hesitate to provide code samples or anything that could help me understand better what your exact use case is.

I never implemented OAuth with knock myself, but it would be interesting to see if there is a way to provide something in future versions that would make it easier.

I will be using Knock for an SPA and 2 mobile apps.

The problem is that I'm storing the social ids and the email and password differently. So the fact that the authenticate method only provides a password parameter doesn't give me the context I need to determine whether I should be checking against a social id or a crypted password.

I'm not actually storing the OAuth token as I don't really need to interact with their API other than to log them in.

Now, I understand that I may not be approaching this correctly which is why I'm having issues thinking through this.

I appreciate the help!

Social authentication is out of the scope of knock for the moment. Although it is definitely something I'd like to implement in the future.

For the moment, what you can do is implement your own controller that retrieves (or creates) a user from social authentication and responds with a JWT token.

Here's how you can generate a JWT token using Knock:

# Create the token
knock_token = Knock::AuthToken.new payload: { sub: user.id }

# Access the JWT token
knock_token.token

The "flow" would probably be something similar to this:

social-auth-flow

(source: http://madhatted.com/2014/6/17/authentication-for-single-page-apps)

Hope this helps! And please let us know how you ended up implementing this :)

Honestly, I see this as a flexibility issue and not something specific to social auth.

If you were to make params available when finding and authenticating a user you'd provide a lot more flexibility.

I'm excited to see where this gem goes. I would love to see a solid JWT implementation for Rails.

Thanks again for putting time into my problem. I'll give your suggestion a shot.

I really appreciate the help!

The v2 is already providing more flexibility as you can access the params to retrieve a user if you need to. Have a look at the changelog if you're interested! This has not been released yet and it is still subject to change.

Awesome! I'll give v2 a look. Would you say it's beta? Alpha?

Alpha, it's stable but the design could change.
On Sun, Sep 27, 2015 at 01:51 Anthony Smith [email protected]
wrote:

Awesome! I'll give v2 a look. Would you say it's beta? Alpha?

—
Reply to this email directly or view it on GitHub
https://github.com/nsarno/knock/issues/11#issuecomment-143465379.

Hey, I created a sample application for facebook login + jwt. I'm not sure if its the best approach but it works: https://github.com/muZk/rails5-api-jwt-facebook-auth

It was designed for getting the FB accessToken "client" side (which could be a JS application or Mobile app).

@nsarno any thoughts?


The key files are:

facebook_user_token_controller.rb

class FacebookUserTokenController < ActionController::API

  before_action :authenticate

  def create
    render json: auth_token, status: :created
  end

  private

  def authenticate
    unless entity.present?
      raise Knock.not_found_exception_class
    end
  end

  def auth_token
    if entity.respond_to? :to_token_payload
      Knock::AuthToken.new payload: entity.to_token_payload
    else
      Knock::AuthToken.new payload: { sub: entity.id }
    end
  end

  def entity
    @entity ||=
      if FacebookService.valid_token?(auth_params[:access_token])
        data = FacebookService.fetch_data(auth_params[:access_token])
        User.find_or_create_by uid: data['id'] do |user|
          user.first_name = data['first_name']
          user.last_name = data['last_name']
          user.email = data['email']
        end
      end
  end

  def auth_params
    params.require(:auth).permit :access_token
  end

end

And facebook_service.rb which has the logic for facebook token validation and data retrieval:

class FacebookService

  def self.valid_token?(access_token)
    status = false
    begin
      # We need to check if the access_token is valid for our FB APP. Source: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#checktoken
      debug_token = Koala::Facebook::API.new(access_token).debug_token(app_access_token_info['access_token'])
      status = true if debug_token['data']['is_valid']
    ensure
      return status
    end
  end

  def self.fetch_data(access_token)
    Koala::Facebook::API.new(access_token).get_object('me', fields: 'name,first_name,last_name,email') if valid_token?(access_token)
  end

  def self.app_access_token_info
    @app_access_token ||= Koala::Facebook::OAuth.new.get_app_access_token_info
  end

end

@muZk it seems that you have mixed things up here in this line:

debug_token = Koala::Facebook::API.new(access_token).debug_token(app_access_token_info['access_token'])

You should be using app_acces_token for Koala::Facebook::API.new and given regular user's acces_token for debug_token method, like this:

debug_token = Koala::Facebook::API.new(app_access_token_info['access_token']).debug_token(access_token)
Was this page helpful?
0 / 5 - 0 ratings