Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?
The short answer is to read this blog post which goes into details of how the concept is minimally implemented
This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...
but for now, here is how I implemented it in some project...
First the angular app part, you can use something like Satellizer which plays nicely...
here is the angular auth module in the front-end app
# coffeescript
config = (
$authProvider
$stateProvider
) ->
$authProvider.httpInterceptor = true # to automatically add the headers for auth
$authProvider.baseUrl = "http://path.to.your.api/"
$authProvider.loginRedirect = '/profile' # front-end route after login
$authProvider.logoutRedirect = '/' # front-end route after logout
$authProvider.signupRedirect = '/sign_in'
$authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
$authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
$authProvider.loginRoute = 'sign_in' # front-end route for login
$authProvider.signupRoute = 'sign_up' # front-end route for sign_up
$authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
$authProvider.tokenRoot = 'data'
$authProvider.tokenName = 'token'
$authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
$authProvider.authHeader = 'Authorization'
$authProvider.authToken = 'Bearer'
$authProvider.storage = 'localStorage'
# state configurations for the routes
$stateProvider
.state 'auth',
url: '/'
abstract: true
templateUrl: 'modules/auth/auth.html'
data:
permissions:
only: ['guest']
redirectTo: 'profile'
.state 'auth.sign_up',
url: $authProvider.signupRoute
views:
'sign_up#auth':
templateUrl: 'modules/auth/sign_up.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
.state 'auth.sign_in',
url: $authProvider.loginRoute
views:
'sign_in#auth':
templateUrl: 'modules/auth/sign_in.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
this is the basic configurations for satellizer... as for the authentication controller... it's something like following
#signIn = (email, password, remember_me) ->
$auth.login
email: email
password: password
remember_me: remember_me
.then(success, error)
return
#signUp = (name, email, password) ->
$auth.signup
name: name
email: email
password: password
.then(success, error)
return
this is the basics for authenticating
as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.
second implement the API controller and the authentication controller
for example it might look something like the following
class Api::V1::ApiController < ApplicationController
# The API responds only to JSON
respond_to :json
before_action :authenticate_user!
protected
def authenticate_user!
http_authorization_header?
authenticate_request
set_current_user
end
# Bad Request if http authorization header missing
def http_authorization_header?
fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
true
end
def authenticate_request
decoded_token ||= AuthenticationToken.decode(authorization_header)
#auth_token ||= AuthenticationToken.where(id: decoded_token['id']).
first unless decoded_token.nil?
fail UnauthorizedError, 'errors.auth.invalid_token' if #auth_token.nil?
end
def set_current_user
#current_user ||= #auth_token.user
end
# JWT's are stored in the Authorization header using this format:
# Bearer some_random_string.encoded_payload.another_random_string
def authorization_header
return #authorization_header if defined? #authorization_header
#authorization_header =
begin
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
else
nil
end
end
end
end
class Api::V1::AuthenticationsController < Api::V1::ApiController
skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]
def sign_in
# getting the current user from sign in request
#current_user ||= User.find_by_credentials(auth_params)
fail UnauthorizedError, 'errors.auth.invalid_credentials' unless #current_user
generate_auth_token(auth_params)
render :authentication, status: 201
end
def sign_out
# this auth token is assigned via api controller from headers
#auth_token.destroy!
head status: 204
end
def generate_auth_token(params)
#auth_token = AuthenticationToken.generate(#current_user, params[:remember_me])
end
end
The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)
here is the implementation for the AuthenticationToken model
class AuthenticationToken < ActiveRecord::Base
## Relations
belongs_to :user
## JWT wrappers
def self.encode(payload)
AuthToken.encode(payload)
end
def self.decode(token)
AuthToken.decode(token)
end
# generate and save new authentication token for the user
def self.generate(user, remember_me = false)
#auth_token = user.authentication_tokens.create
#auth_token.token = AuthToken.generate(#auth_token.id, remember_me)
#auth_token.save!
#auth_token
end
# check if a token can be used or not
# used by background job to clear the authentication collection
def expired?
AuthToken.decode(token).nil?
end
end
it uses a wrapper called AuthToken which wraps the JWT functionality
here is it's implementation
# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def self.decode(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
rescue JWT::ExpiredSignature
# It will raise an error if it is not a token that was generated
# with our secret key or if the user changes the contents of the payload
Rails.logger.info "Expired Token"
nil
rescue
Rails.logger.warn "Invalid Token"
nil
end
def self.generate(token_id, remember_me = false)
exp = remember_me ? 6.months.from_now : 6.hours.from_now
payload = { id: token_id.to_s, exp: exp.to_i }
self.encode(payload)
end
end
Related
I use the gem jwt、devise to build a user login system,
I generate a model Authentication to check the token exist or not.
follow this code:
models/authentication.rb
class Authentication < ApplicationRecord
def self.generate_access_token(email)
payload = {:email => email}
secret = 'secret'
token = JWT.encode payload, secret, 'HS256'
return token
end
end
controllers/user/sessions_controller.rb
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
#token = Authentication.generate_access_token(user.email)
Authentication.create(access_token: #token)
authentications = {token: #token, email: user.email}
render json: authentications, status: :created
else
head(:unauthorized)
end
end
when I do a post request to user/sessions I will get token and user email and store it in localstorage of client, and help me to check the token is valid.
follow this code:
def authenticate_token
token = Authentication.find_by_access_token(params[:token])
head :unauthorized unless token
end
In my question, are there ways to let token don't need to store into database?
You can decode the token and get the email stored in it, and find user by that email.
Suppose you carry the token in the Authorization header, like
Authorization: Bearer <token>
then you can define a before_action to do this:
class ApplicationController < ActionController::API
before_action :authenticate_token
def authenticate_token
token = request.headers['Authorization'].to_s =~ /^Bearer (.*)$/i && $1
return head :unauthorized unless token
payload = JWT.decode(token, 'secret', true, algorithm: 'HS256')
user = User.find_by(email: payload['email'])
return head :unauthorized unless user
# TODO set the `user` as current_user
# How to patch devise's `current_user` helper is another story
end
end
If I were you, I would put user ID in the token, not email, because ID is shorter, and faster to lookup from database, and it exposes nothing personal to the internet (note that JWT is not encrypted. It's just signed).
Or you can skip all these messy things by just using knock instead of devise.
I am unable to override the Rails serializer when using devise_token_auth and active_model_serializer for Devise sign_up method.
I would like to customize the returned fields from the Devise sign_up controller when querying my API.
The devise_token_auth gem documentation indicates:
To customize json rendering, implement the following protected controller methods
Registration Controller
...
render_create_success
...
Note: Controller overrides must implement the expected actions of the controllers that they replace.
That is all well and good, but how do I do this?
I've tried generating a UserController serializer like the following:
class UsersController < ApplicationController
def default_serializer_options
{ serializer: UserSerializer }
end
# GET /users
def index
#users = User.all
render json: #users
end
end
but it's only being used for custom methods such as the index method above: it's not being picked up by devise methods like sign_up
I would appreciate a detailed response since I've looked everywhere but I only get a piece of the puzzle at a time.
For the specific serialiser question, here's how I did it:
overrides/sessions_controller.rb
module Api
module V1
module Overrides
class SessionsController < ::DeviseTokenAuth::SessionsController
# override this method to customise how the resource is rendered. in this case an ActiveModelSerializers 0.10 serializer.
def render_create_success
render json: { data: ActiveModelSerializers::SerializableResource.new(#resource).as_json }
end
end
end
end
end
config/routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/overrides/sessions'
}
# snip the rest
Devise sign_up corresponds to devise_token_auth registrations controller and Devise sign_in corresponds to devise_token_auth sessions controller. Therefore when using this gem, customizing Devise sign_in and sign_up methods requires customizing both of these devise_token_auth controllers.
There are two ways to go about this based on what you need to accomplish.
Method #1
If you want to completely customize a method in the controller then follow the documentation for overriding devise_token_auth controller methods here: https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides
This is what I did and it's working fine:
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
This will route all devise_token_auth sessions and registrations to LOCAL versions of the controllers if a method exists in your local controller override. If the method does not exist in your local override, then it will run the method from the gem. You basically have to copy the controllers from the gem into 'app/controllers/overrides' and make any changes to any method you need to customize. Erase the methods from the local copy you are not customizing. You can also add callbacks in this way. If you want to modify the response, customize the the render at the end of the method that will return the response as json via active_model_serializer.
This is an example of my sessions controller which adds a couple of custom before_actions to add custom functionality:
#app/controllers/overrides/sessions_controller.rb
module Overrides
class SessionsController < DeviseTokenAuth::SessionsController
skip_before_action :authenticate_user_with_filter
before_action :set_country_by_ip, :only => [:create]
before_action :create_facebook_user, :only => [:create]
def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
#resource = nil
if field
q_value = resource_params[field]
if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end
#q = "#{field.to_s} = ? AND provider='email'"
q = "#{field.to_s} = ? AND provider='#{params[:provider]}'"
#if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
# q = "BINARY " + q
#end
#resource = resource_class.where(q, q_value).first
end
#sign in will be successful if #resource exists (matching user was found) and is a facebook login OR (email login and password matches)
if #resource and (params[:provider] == 'facebook' || (valid_params?(field, q_value) and #resource.valid_password?(resource_params[:password]) and (!#resource.respond_to?(:active_for_authentication?) or #resource.active_for_authentication?)))
# create client id
#client_id = SecureRandom.urlsafe_base64(nil, false)
#token = SecureRandom.urlsafe_base64(nil, false)
#resource.tokens[#client_id] = { token: BCrypt::Password.create(#token), expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i }
#resource.save
sign_in(:user, #resource, store: false, bypass: false)
yield #resource if block_given?
#render_create_success
render json: { data: resource_data(resource_json: #resource.token_validation_response) }
elsif #resource and not (!#resource.respond_to?(:active_for_authentication?) or #resource.active_for_authentication?)
render_create_error_not_confirmed
else
render_create_error_bad_credentials
end
end
def set_country_by_ip
if !params['fb_code'].blank?
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, country_name: country_id.name, test: 'Test')
I18n.locale = country_id.language_code
else
params.merge!(country_id: 1, country_name: 'International')
end
else
params.merge!(country_id: 1, country_name: 'International')
end
end
end
def create_facebook_user
if !params['fb_code'].blank?
# TODO capture errors for invalid, expired or already used codes to return beter errors in API
user_info, access_token = Omniauth::Facebook.authenticate(params['fb_code'])
if user_info['email'].blank?
Omniauth::Facebook.deauthorize(access_token)
end
#if Facebook user does not exist create it
#user = User.find_by('uid = ? and provider = ?', user_info['id'], 'facebook')
if !#user
#graph = Koala::Facebook::API.new(access_token, ENV['FACEBOOK_APP_SECRET'])
Koala.config.api_version = "v2.6"
new_user_picture = #graph.get_picture_data(user_info['id'], type: :normal)
new_user_info = {
uid: user_info['id'],
provider: 'facebook',
email: user_info['email'],
name: user_info['name'],
first_name: user_info['first_name'],
last_name: user_info['last_name'],
image: new_user_picture['data']['url'],
gender: user_info['gender'],
fb_auth_token: access_token,
friend_count: user_info['friends']['summary']['total_count'],
friends: user_info['friends']['data']
}
#user = User.new(new_user_info)
#user.password = Devise.friendly_token.first(8)
#user.country_id = params['country_id']
#user.country_name = params['country_name']
if !#user.save
render json: #user.errors, status: :unprocessable_entity
end
end
#regardless of user creation, merge facebook parameters for proper sign_in in standard action
params.merge!(provider: 'facebook', email: #user.email)
else
params.merge!(provider: 'email')
end
end
end
end
Notice the use of params.merge! in the callback to add custom parameters to the main controller methods. This is a nifty trick that unfortunately will be be deprecated in Rails 5.1 as params will no longer inherit from hash.
Method #2
If you just want to add functionality to a method in your custom controller, you can get away with subclassing a controller, inheriting from the original controller and passing a block to super as described here:
https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers
I have done this to the create method in my custom registrations controller.
Modify the routes as in method #1
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
and customize the create method in the custom controller:
#app/controllers/overrides/registrations_controller.rb
module Overrides
class RegistrationsController < DeviseTokenAuth::RegistrationsController
skip_before_action :authenticate_user_with_filter
#will run upon creating a new registration and will set the country_id and locale parameters
#based on whether or not a user_ip param is sent with the request
#will default to country_id=1 and locale='en' (International) if it's not sent.
before_action :set_country_and_locale_by_ip, :only => [:create]
def set_country_and_locale_by_ip
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
#TODO check if there's an internet connection here or inside the library function
#params.merge!(country_id: 1, country_name: 'International', locale: 'en')
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, locale: country_id.language_code, country_name: country_id.name)
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
end
#this will add behaviour to the registrations controller create method
def create
super do |resource|
create_assets(#resource)
end
end
def create_assets(user)
begin
Asset.create(user_id: user.id, name: "stars", qty: 50)
Asset.create(user_id: user.id, name: "lives", qty: 5)
Asset.create(user_id: user.id, name: "trophies", qty: 0)
end
end
end
end
I am building an API which authenticates via LinkedIn and then returns our tokens to the client.
The client will be written in AngularJS and will run separately.
Auth URL: http://example.com/users/auth/linkedin
I get the following error after authentication:
Missing template users/omniauth_callbacks/linkedin,
devise/omniauth_callbacks/linkedin, devise/linkedin,
application/linkedin with {
:locale=>[:en],
:formats=>[:html],
:variants=>[],
:handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]
}.
Searched in: * "/usr/local/lib/ruby/gems/2.2.0/gems/web-console-2.0.0/lib/action_dispatch/templates" * "/usr/local/Cellar/ruby/2.2.0/lib/ruby/gems/2.2.0/gems/devise-3.4.1/app/views"
I use the following special gems:
# Authentication
gem 'devise'
gem 'devise-token_authenticatable'
gem 'omniauth'
gem 'omniauth-oauth'
gem 'omniauth-linkedin'
# API Wrappers
gem 'linkedin'
The Source Code
config/routes.rb
# filename: routes.rb
# encoding: utf-8
Rails.application.routes.draw do
#
# API v1 Endpoints
#
scope '/api' do
## Authentication
devise_for :users,
:skip => [:sessions, :password, :registrations, :confirmation],
:controllers => {
:omniauth_callbacks => "users/omniauth_callbacks",
:registrations => "users/registrations",
:sessions => "users/sessions"
}
## User specific routes
scope '/users' do
devise_scope :user do
post '/check' => 'users/users#is_user', as: 'is_user'
post '/current' => 'users/sessions#get_current_user', as: 'current_user'
end
end
end
end
app/controllers/application_controller.rb
# filename: application_controller.rb
# encoding: utf-8
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_filter :set_cors_headers
before_filter :cors_preflight
def set_cors_headers
headers['Access-Control-Allow-Origin'] = AppConfig.client['origin']
headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
headers['Access-Control-Allow-Headers'] = '*'
headers['Access-Control-Max-Age'] = "3628800"
end
def cors_preflight
head(:ok) if request.method == :options
end
end
app/controllers/users/omniauth_callbacks_controller.rb
# filename: omniauth_callbacks_controller.rb
# encoding: utf-8
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
## Sign in / up with LinkedIn
def linkedin
auth_hash = request.env["omniauth.auth"]
auth = Authorization.find_by_provider_and_uid("linkedin", auth_hash['uid'])
if auth
## User already exists
user = auth.user
else
## User already signed in
unless current_user
unless user = User.find_by_email(auth_hash['info']['email'])
user = User.create({
first_name: auth_hash['info']['first_name'],
last_name: auth_hash['info']['last_name'],
email: auth_hash['info']['email'],
password: Devise.friendly_token[0,8]
})
end
## Sign user in
else
user = current_user
end
# Create an authorization for the current user
unless auth = user.authorizations.find_by_provider(auth_hash["provider"])
auth = user.authorizations.build(provider: auth_hash["provider"])
user.authorizations << auth
end
auth.update_attributes({
uid: auth_hash['uid'],
token: auth_hash['credentials']['token'],
secret: auth_hash['credentials']['secret'],
})
# Return user
user
end
end
end
app/controllers/users/registrations_controller.rb
# filename: registrations_controller.rb
# encoding: utf-8
class Users::RegistrationsController < Devise::RegistrationsControllerú
skip_before_filter :verify_authenticity_token
respond_to :json
def create
# Create the user
build_resource(sign_up_params)
# Try to save them
if resource.save
sign_in resource
render status: 200,
json: {
success: true,
info: "Registered",
data: {
user: resource,
auth_token: current_user.authentication_token
}
}
else
# Otherwise fail
render status: :unprocessable_entity,
json: {
success: false,
info: resource.errors,
data: {}
}
end
end
app/controllers/users/sessions_controller.rb
# filename: omniauth_callbacks_controller.rb
# encoding: utf-8
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
## Sign in / up with LinkedIn
def linkedin
auth_hash = request.env["omniauth.auth"]
auth = Authorization.find_by_provider_and_uid("linkedin", auth_hash['uid'])
if auth
## User already exists
user = auth.user
else
## User already signed in
unless current_user
unless user = User.find_by_email(auth_hash['info']['email'])
user = User.create({
first_name: auth_hash['info']['first_name'],
last_name: auth_hash['info']['last_name'],
email: auth_hash['info']['email'],
password: Devise.friendly_token[0,8]
})
end
## Sign user in
else
user = current_user
end
# Create an authorization for the current user
unless auth = user.authorizations.find_by_provider(auth_hash["provider"])
auth = user.authorizations.build(provider: auth_hash["provider"])
user.authorizations << auth
end
auth.update_attributes({
uid: auth_hash['uid'],
token: auth_hash['credentials']['token'],
secret: auth_hash['credentials']['secret'],
})
# Return user
user
end
end
end
app/controllers/users/users_controller.rb
# filename: users_controller.rb
# encoding: utf-8
class Users::UsersController < Devise::SessionsController
protect_from_forgery with: :exception, except: [:is_user]
respond_to :json
## Check if user exists by email
def is_user
#authenticate_user!
render status: 200, json: {
success: !User.find_by_email(params[:email]).blank?
}
end
end
Your linkedin method in the Users::OmniauthCallbacksController class doesn't render or redirect explicitly so it is attempting to do the implicit rendering of the linkedin template (usually linkedin.html.erb.
Based on your code you probably want to render user.to_json or something to that effect in your code so the angular api receives something it can work with.
I am trying to authenticate my new Shopify app. First, my authenticate method redirects the shop owner to Shopify's authentication page:
def authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("someshop.myshopify.com")
redirect_to session.create_permission_url(["read_orders"], "https://myapp.com/shopify/post_authenticate?user=someshop")
end
Once the shop owner has approved the integration, the redirect uri triggers my post_authenticate method:
def post_authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("#{params[:user]}.myshopify.com")
token = session.request_token(:code => params[:code], :signature => params[:signature], :timestamp => params[:timestamp])
end
But the request_token method returns the following error:
#<ShopifyAPI::ValidationException: Invalid Signature: Possible malicious login>
I have read somewhere that you need to be in the same ShopifyAPI session while doing all of this, but it does not say so in the documentation. And the example app takes a very different approach than the documentation.
As per my comment, I utilize the omniauth methodology for authenticating. Here's a gist of the code for reference. https://gist.github.com/agmcleod/7106363317ebd082d3df. Put all the snippets below.
class ApplicationController < ActionController::Base
protect_from_forgery
force_ssl
helper_method :current_shop, :shopify_session
protected
def current_shop
#current_shop ||= Shop.find(session[:shop_id]) if session[:shop_id].present?
end
def shopify_session
if current_shop.nil?
redirect_to new_login_url
else
begin
session = ShopifyAPI::Session.new(current_shop.url, current_shop.token)
ShopifyAPI::Base.activate_session(session)
yield
ensure
ShopifyAPI::Base.clear_session
end
end
end
end
In my login controller:
def create
omniauth = request.env['omniauth.auth']
if omniauth && omniauth[:provider] && omniauth[:provider] == "shopify"
shop = Shop.find_or_create_by_url(params[:shop].gsub(/https?\:\/\//, ""))
shop.update_attribute(:token, omniauth['credentials'].token)
shopify_session = ShopifyAPI::Session.new(shop.url, shop.token)
session[:shop_id] = shop.to_param
redirect_to root_url
else
flash[:error] = "Something went wrong"
redirect_to root_url
end
end
config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :shopify, Settings.api_key, Settings.api_secret,
scope: 'write_products,write_script_tags,read_orders',
setup: lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end
Then in your routes file, map the create action of your session appropriately:
match '/auth/shopify/callback', :to => 'login#create'
From there i use the shopify_session method as an around filter on the appropriate controllers.
I have the following code:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
end
(I have setup Koala as described here https://github.com/arsduo/koala/wiki/Koala-on-Rails)
I don't really want to introduce OmniAuth as what I am trying to do is fairly simple. The above code works, the problem is that it is calling Facebook for every page load = not good. I'm guessing I need to store session[:user_id] and then just call User.find(session[:user_id]) for each subsequent request after the user has authenticated?
Can anyone suggest the most efficient way of solving this so I'm not waiting for Facebook on each page load?
You could try something like this:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user, if: Proc.new{ !current_user? }
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
def current_user?
!!#current_user
end
end
I have an implementation that is use for my facebook authentication and authorization.
The code is seperated in non state changing methods and statechanging methods.
Feel free to use the code.
## checks if we have access to fb_info and it is not expired
## Non stagechanging
def oauth_is_online
return !(session[:fb_cookies].nil? or session[:fb_cookies]['issued_at'].to_i + session[:fb_cookies]['expires'].to_i < Time.now.to_i - 10)
end
## checks if the user is in the database
## Non stats changing
def oauth_is_in_database
return oauth_is_online && 0 < User.where(:fb_id => session[:fb_cookies]['user_id']).count
end
## Returns true if it is the specified user
## Non stagechanging
def oauth_is_user(user)
return oauth_is_online && session[:fb_cookies]['user_id'] == user.fb_id
end
## Requires the user to be online. If not, hell breaks loose.
def oauth_require_online
if !oauth_ensure_online
render :file => "public/401.html", :status => :unauthorized, :layout => false
return false
end
return session[:fb_cookies]
end
## Requires the user to be online and the correct user. If not, hell breaks loose.
def oauth_require_user(user)
c = oauth_require_online
return false unless c
return c unless !oauth_is_user(user)
render :file => "public/403.html", :status => :unauthorized, :layout => false
return false
end
## Ensures that the user is online
def oauth_ensure_online
begin
# Checks if the user can be verified af online
if !oauth_is_in_database
# the user is not online. Now make sure that they becomes online.
logger.info "Creates new FB Oauth cookies"
fb_cookies = Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies)
# TODO In the future the session should be reset at this point
# reset_session
session[:fb_cookies_reload] = false
session[:fb_cookies] = fb_cookies
logger.info "Updating user in database."
# Tries to load the user
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
logger.info "User exists? => #{#current_user.nil?}"
# creating if not found
#current_user = User.new(:fb_id => session[:fb_cookies]['user_id']) unless !#current_user.nil?
# Loading graph
#fb_graph = Koala::Facebook::API.new(session[:fb_cookies]['access_token'])
# Loading info from graph
me = #fb_graph.get_object('me')
#current_user.name_first = me['first_name'].to_s
#current_user.name_last = me['last_name'].to_s
#current_user.email = me['email'].to_s
#current_user.fb_access_token = session[:fb_cookies]['access_token'].to_s
# Saving the updated user
#current_user.save!
return oauth_is_online
else
# the user is online. Load variables.
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
#fb_graph = Koala::Facebook::API.new(#current_user.fb_access_token)
end
return oauth_is_online
rescue Koala::Facebook::OAuthTokenRequestError => oe
## TODO handle the diferent errors
# user is not online on facebook
# user have not authenticatet us
# token is used allready once.
logger.debug "FB koala OAuthTokenRequestError: #{oe}"
return false
end
end