Switching between web and touch interfaces on Facebook login using Omniauth and Rails 3
The situation:
Using Rails 3 and OmniAuth, I have an app that authenticates using the Facebook strategy. This app has been built to work equally-well for web and开发者_开发知识库 mobile interfaces (ala Jquery-Mobile).
The challenge is to get OmniAuth to provide the mobile version of Facebook's login screen to mobile devices and the web version to desktop devices.
I've hacked together a solution which I'll put as an answer.
Actually, since OmniAuth::Strategies are already Rack middleware, its even simpler. Just override the request_phase method and check the @env instance variable present in the strategy for a mobile user_agent:
module OmniAuth
module Strategies
class Facebook < OAuth2
MOBILE_USER_AGENTS = 'webos|ipod|iphone|mobile'
def request_phase
options[:scope] ||= "email,offline_access"
options[:display] = mobile_request? ? 'touch' : 'page'
super
end
def mobile_request?
ua = Rack::Request.new(@env).user_agent.to_s
ua.downcase =~ Regexp.new(MOBILE_USER_AGENTS)
end
end
end
end
For modern devise / omniauth (>= 1.0) use this in your config/initializers/devise.rb:
FACEBOOK_SETUP_PROC = lambda do |env|
request = Rack::Request.new(env)
mobile_device = request.user_agent =~ /Mobile|webOS/i
request.env['omniauth.strategy'].options[:display] = mobile_device ? "touch" : "page"
end
config.omniauth :facebook, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET,
:scope => 'email,offline_access', :setup => FACEBOOK_SETUP_PROC,
:client_options => { :ssl => { :ca_file => Rails.root.join("config/ca-bundle.crt").to_s }}
I tried the first solution but I couldn't get it working. After much searching I found out that Omniauth has an option ":setup => true" which allows for dynamic setting of arguments, such as the :display option necessary for Facebook OAuth.
First turn on the :setup option.
config.omniauth :facebook, APP_CONFIG["fb_app_id"], APP_CONFIG["fb_app_secret"],
{:scope => 'email, offline_access', :setup => true}
Then add a second route (setup route):
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } do
get '/users/auth/:provider' => 'users/omniauth_callbacks#passthru'
get '/users/auth/:provider/setup' => 'users/omniauth_callbacks#setup'
end
Add this controller. You might already have it if you followed the devise manual.
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def setup
request.env['omniauth.strategy'].options[:display] = mobile_device? ? "touch" : "page"
render :text => "Setup complete.", :status => 404
end
end
Add this method into your ApplicationController:
def mobile_device?
if session[:mobile_param]
session[:mobile_param] == "1"
else
request.user_agent =~ /Mobile|webOS/
end
end
Done!
The approved answer works, except I had to change one line. For the current version of omniauth-facebook, I had to set the display option, like this:
options[:authorize_params] = mobile_request? ? { :display => 'touch' } : { :display => 'page' }
You can use 'popup', 'touch', or 'page' from what I've found.
My solution is pretty involved and requires both modifying the OmniAuth Facebook strategy and adding Rack middleware.
First, I added a class attribute and changed a method in OmniAuth::Strategies::Facebook (I put this at the end of my omniauth.rb config file, but it belongs in the lib directory):
module OmniAuth
module Strategies
class Facebook < OAuth2
cattr_accessor :display # new
def request_phase
options[:scope] ||= "email,offline_access"
options[:display] = OmniAuth::Strategies::Facebook.display || nil # new
super
end
end
end
end
Second, I added a piece of Rack middleware to determine if the request was from a mobile device and then set the display accordingly:
module Rack
class FacebookMobileOmniauth
def initialize(app)
@app = app
end
MOBILE_USER_AGENTS = 'palm|blackberry|nokia|phone|midp|mobi|symbian|chtml|ericsson|minimo|' +
'audiovox|motorola|samsung|telit|upg1|windows ce|ucweb|astel|plucker|' +
'x320|x240|j2me|sgh|portable|sprint|docomo|kddi|softbank|android|mmp|' +
'pdxgw|netfront|xiino|vodafone|portalmmm|sagem|mot-|sie-|ipod|up\\.b|' +
'webos|amoi|novarra|cdm|alcatel|pocket|ipad|iphone|mobileexplorer|' +
'mobile'
def call(env)
request = Request.new(env)
if request.user_agent.to_s.downcase =~ Regexp.new(MOBILE_USER_AGENTS)
OmniAuth::Strategies::Facebook.display = 'touch'
else
OmniAuth::Strategies::Facebook.display = nil
end
return @app.call(env)
end
end
end
And lastly, I added the Rack middleware to my config.ru:
require ::File.expand_path('../config/environment', __FILE__)
use Rack::FacebookMobileOmniauth # new
run Mystupid::Application
精彩评论