module Plezi ######################################### # This is a social Authentication Controller for OAuth2 services. # This controller allows you to easily deploy Facebook Login, Google Login and any other OAuth2 complient login service. # # To include this controller in your application, you need to require it using: # # require 'plezi/oauth' # # It is possible to manualy register any OAuth 2.0 authentication service using the `register_service` method: # # register_service(:foo, # app_id: 'registered app id / client id', # app_secret: 'registered app secret / client secret', # auth_url: "https://foo.bar.com/o/oauth2/auth", # token_url: "https://foo.bar.com/oauth2/v3/token", # profile_url: "https://foo.bar/oauth2/v1/userinfo", # scope: "profile email") # # The `.register_service` method will be automatically called for the following login services: # # - Facebook authentication using the Graph API, v. 2.3 - [see Facebook documentation](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.3). # - Google authentication using the OAuth 2.0 API - [see Google documentation](https://developers.google.com/identity/protocols/OAuth2WebServer). # # To enjoy autorgistration for Facebook or Google, make sure the following environment variables are set: # ENV['FB_APP_ID'] = {facebook_app_id} # ENV['FB_APP_SECRET'] = {facebook_app_secret} # ENV['GOOGLE_APP_ID'] = {google_app_id} # ENV['GOOGLE_APP_SECRET'] = {google_app_secret} # # To add the OAuth routes to the routes, use the following short-cut method (add it to the `routes.rb`) file: # # create_auth_shared_route do |service, remote_user_id, email, full_remote_response| # # perform any specific app authentication logic such as saving the info. # # return the current user or false if the callback is called with an authentication failure. # end # # \* Notice that, as always, route placement order effects behavior, so that routes are checked according to order of creation. # # The `create_auth_shared_route` method is a shortcut taht calls the `#shared_route` method with the relevant arguments and sets the OAuth2Ctrl callback. # # Use the following links for social authentication: # # - Facebook: "/auth/facebook" # - Google: "/auth/google" # - foo_service: "/auth/foo_service" # # You can control the page to which the user will return once authentication is complete # (even when authentication fails) by setting the "redirect_after" parameter into the GET request in the url. for example: # # - Google: "/auth/google?redirect_after=/foo/bar" class OAuth2Ctrl # Sets (or gets) the global callback to be called after authentication is attempeted. # # Accepts a block that will be called with the following parameters: # service_name:: the name of the service. i.e. :facebook, :google, etc'. # service_token:: the authentication token returned by the service. This token should be stored for future access # remote_user_id:: service's user id. # remote_user_email:: user's primamry email, as (and if) registed with the service. # remote_response:: a Hash with the complete user data response (including email and id). # # If the authentication fails for the service, the block will be called with the following values `auth_callback.call(nil, ni, {server: :responce, might_be: :empty})` # # The block will be run in the context of the controller and all the controller's methods will be available to it. # # i.e.: # OAuth2Ctrl.auth_callback |service, service_token, id, email, full_res| PL.info "OAuth got: #{full_res.to_s}"; cookies["#{service}_pl_auth_token".to_sym], cookies["#{service}_user_id".to_sym], cookies["#{service}_user_email".to_sym] = service_token, id, email } # # defaults to the example above, which isn't a very sercure behavior, but allows for easy testing. def self.auth_callback &block block_given? ? (@@auth_callback = block) : ( @@auth_callback ||= (Proc.new {|service, service_token, id, email, res| Plezi.info "deafult callback called for #{service}, with response: #{res.to_s}"; session["#{service}_pl_auth_token"], session["#{service}_user_id"], session["#{service}_user_email"] = service_token, id, email}) ) end # Stores the registered services library SERVICES = {} # This method registers a social login service that conforms to the OAuth2 model. # # Accepts the following required parameters: # service_name:: a Symbol naming the service. i.e. :facebook or :google . # options:: a Hash of options, some of which are required. # # The options are: # app_id:: Required. The aplication's unique ID (sometimes called `client_id`) registered with the service. i.e. ENV [FB_APP_ID] (storing these in environment variables is safer then hardcoding them) # app_secret:: Required. The aplication's unique secret registered with the service (sometimes called `client_secret`). # auth_url:: Required. The authentication URL. This is the url to which the user is redirected. i.e.: "https://www.facebook.com/dialog/oauth" # token_url:: Required. The token request URL. This is the url used to switch the single-use code into a persistant authentication token. i.e.: "https://www.googleapis.com/oauth2/v3/token" # profile_url:: Required. The URL used to ask the service for the user's profile (the service's API url). i.e.: "https://graph.facebook.com/v2.3/me" # scope:: a String representing the scope requested. i.e. 'email profile'. # # There will be an attempt to automatically register Facebook and Google login services under these conditions: # # * For Facebook: Both ENV ['FB_APP_ID'] && ENV ['FB_APP_SECRET'] have been defined. # * For Google: Both ENV ['GOOGLE_APP_ID'] && ENV ['GOOGLE_APP_SECRET'] have been defined. # # # The auto registration uses the following urls (updated to June 5, 2015): # # * facebook auth_url: "https://www.facebook.com/dialog/oauth" # * facebook token_url: "https://graph.facebook.com/v2.3/oauth/access_token" # * facebook profile_url: "https://graph.facebook.com/v2.3/me" # * google auth_url: "https://accounts.google.com/o/oauth2/auth" # * google token_url: "https://www.googleapis.com/oauth2/v3/token" # * google profile_url: "https://www.googleapis.com/plus/v1/people/me" # # to change the default scope or url's for Facebook or Google, simpley re-register the service using this method. # def self.register_service service_name, options raise "Cannot register service, missing required information." unless service_name && options[:auth_url] && options[:token_url] && options[:profile_url] && options[:app_id] && options[:app_secret] # for google, scope is space delimited. for facebook it's comma delimited options[:scope] ||= 'profile, email' SERVICES[service_name] = options end # Called to manually run through the authentication logic for the requested service, # without performing any redirections. # # Use this method to attempt and re-login a user using an existing login token: # # token = 'google_token_could_be_recieved_also_from_javascript_sdk' # OAuth2Ctrl.auth :google, token, self # # The method will return false if re-login fails and it will otherwise return the callback's return value. # # Call this method from within a controller, passing the controller (self) to the method, like so: # # OAuth2Ctrl.auth :facebook, token, self # # This is especially effective if `auth_callback` returns the user object, as it would allow to chain # different login methods, i.e.: # # @user ||= app_login || OAuth2Ctrl.auth(:facebook, fb_token, self) || OAuth2Ctrl.auth(:google, google_token, self) || .... def self.auth service_name, service_token, controller service = SERVICES[service_name] return false unless service # auth_res = controller.cookies[c_name] ? (JSON.parse URI.parse("#{service[:profile_url]}?access_token=#{controller.cookies[c_name]}").read rescue ({}) ) : {} # controller.cookies[c_name] = nil unless auth_res['id'] # auth_res['id'] ? controller.instance_exec( service_name, auth_res['id'], auth_res['email'], auth_res, &auth_callback) : ( controller.instance_exec( service_name, nil, nil, auth_res, &auth_callback) && false) auth_res = {} uri = URI.parse("#{service[:profile_url]}?access_token=#{service_token}") Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == "https"), verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| req = Net::HTTP::Get.new(uri) res = http.request(req).body auth_res = (JSON.parse res) rescue ({}) end if service_token auth_res['id'] ? controller.instance_exec( service_name, service_token, auth_res['id'], auth_res['email'], auth_res, &auth_callback) : ( controller.instance_exec( service_name, nil, nil, nil, auth_res, &auth_callback) && false) end def update service_name = params[:id].to_s.to_sym service = SERVICES[service_name] return false unless service if params[:error] instance_exec( service_name, nil, nil, nil, {}, &self.class.auth_callback) return redirect_to(flash[:redirect_after]) end unless params[:code] flash[:redirect_after] = params[:redirect_after] || '/' return redirect_to _auth_url_for(service_name) end uri = URI.parse service[:token_url] service_token = nil Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == "https"), verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| req = Net::HTTP::Post.new(uri) req.form_data = {"client_id" => service[:app_id], "client_secret" => service[:app_secret], "code" => params[:code] , "grant_type" => "authorization_code","redirect_uri" => "#{request.base_url}/auth/#{service_name.to_s}"} res = http.request(req).body service_token = ((JSON.parse res)['access_token'] rescue nil) end self.class.auth service_name, service_token, self redirect_to(flash[:redirect_after]) end alias :show :update protected # returns a url for the requested service's social login. # # this is used internally. Normal behavior would be to set a link to '/auth/{service_name}', where correct redirection will be automatic. # def _auth_url_for service_name service = SERVICES[service_name] return nil unless service redirect_uri = Plezi::Base::Helpers.encode_url "#{request.base_url}/auth/#{service_name.to_s}" #response_type return "#{service[:auth_url]}?client_id=#{service[:app_id]}&redirect_uri=#{redirect_uri}&scope=#{service[:scope]}&response_type=code" end register_service(:facebook, app_id: ENV['FB_APP_ID'], app_secret: ENV['FB_APP_SECRET'], auth_url: "https://www.facebook.com/dialog/oauth", token_url: "https://graph.facebook.com/v2.3/oauth/access_token", profile_url: "https://graph.facebook.com/v2.3/me", scope: "public_profile,email") if ENV['FB_APP_ID'] && ENV['FB_APP_SECRET'] register_service(:google, app_id: ENV['GOOGLE_APP_ID'], app_secret: ENV['GOOGLE_APP_SECRET'], auth_url: "https://accounts.google.com/o/oauth2/auth", token_url: "https://www.googleapis.com/oauth2/v3/token", profile_url: "https://www.googleapis.com/oauth2/v1/userinfo", scope: "profile email") if ENV['GOOGLE_APP_ID'] && ENV['GOOGLE_APP_SECRET'] end end # This method creates the OAuth2Ctrl route. # This is actually a short-cut for: # # shared_route "auth/(:id)/(:code)" , Plezi::OAuth2Ctrl # Plezi::OAuth2Ctrl.auth_callback = Proc.new {|service, user_id, user_email, service_response| ... } # # the `:id` parameter is used to identify the service (facebook, google. etc'). # # The method accepts a block that will be used to set the authentication callback. See the Plezi::OAuth2Ctrl documentation for details. # # The method can be called only once and will self-destruct. def create_auth_shared_route options = {}, &block shared_route "auth/(:id)" , Plezi::OAuth2Ctrl undef create_auth_shared_route Plezi::OAuth2Ctrl.auth_callback &block if block Plezi::OAuth2Ctrl end