diff --git a/.travis.yml b/.travis.yml index be375af..5c0f254 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ +before_install: + - gem update bundler + - bundle --version + - gem update --system 2.1.11 + - gem --version rvm: - 1.8.7 - 1.9.2 - 1.9.3 + - 2.0.0 - jruby -branches: - only: - - master diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..836f2e1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,99 @@ +## 1.6.0 (2014-01-13) + +Features: + + - ability to specify `auth_type` per-request (#78, @sebastian-stylesaint) + - image dimension can be set using `image_size` option (#91, @weilu) + - update Facebook authorize URL to fix broken authorization (#103, @dlackty) + - adds `info_fields` option (#109, @bloudermilk) + - adds `locale` parameter (#133, @donbobka, @simi) + - add automatically `appsecret_proof` (#140, @nlsrchtr, @simi) + +Changes: + + - `NoAuthorizationCodeError` and `UnknownSignatureAlgorithmError` will now `fail!` (#117, @nchelluri) + - don't try to parse the signature if it's nil (#127, @oriolgual) + +## 1.5.1 (2013-11-18) + +Changes: + + - don't use `access_token` in URL [CVE-2013-4593](https://github.com/mkdynamic/omniauth-facebook/wiki/Access-token-vulnerability:-CVE-2013-4593) (@homakov, @mkdynamic, @simi) + +## 1.5.0 (2013-11-13) + +Changes: + + - remove `state` param to fix CSRF vulnerabilty [CVE-2013-4562](https://github.com/mkdynamic/omniauth-facebook/wiki/CSRF-vulnerability:-CVE-2013-4562) (@homakov, @mkdynamic, @simi) + +## 1.4.1 (2012-07-07) + +Changes: + + - update to omniauth-oauth2 1.1.0 for csrf protection (@mkdynamic) + +## 1.4.0 (2012-06-24) + +Features: + + - obey `skip_info?` config (@mkdynamic) + - add support of the `:auth_type` option to `:authorize_options` (#58, @JHeidinga, @mkdynamic) + - support `access_token` parameter as part of the callback request (#62, @steverandy) + +## 1.3.0 (2012-05-05) + +Features: + + - dynamic permissions in the auth params (#30, @famoseagle) + - add support for facebook canvas (@mkdynamic) + - add verified key to the info hash (#34, @ryansobol) + - add option to use secure url for image in auth hash (@mkdynamic) + - add option to specify image size (@mkdynamic) + +Changes: + + - have `raw_info` return an empty hash if the Facebook response returns false (#44, @brianjlandau) + - prevent oauth2 from interpreting Facebook's expires field as `expires_in`, when it's really `expires_at` (#39, @watsonbox) + - remove deprecated `offline_access` permission (@mkdynamic) + +Changes: + + - tidy up the `callback_url` option (@mkdynamic) + +## 1.2.0 (2012-01-06) + +Features: + + - add `state` to authorization params (#19, @GermanDZ) + +Changes: + + - lock to `rack ~> 1.3.6` (@mkdynamic) + +## 1.1.0 (2011-12-10) + +Features: + + - add `callback_url` option (#13, @gumayunov) + - support for parsing code from signed request cookie (client-side flow) (@mkdynamic) + +## 1.0.0 (2011-11-19) + +Features: + + - allow passing of display via option (@mkdynamic) + +Bugfixes: + + - fix `ten_mins_from_now` calculation (#7, @olegkovalenko) + +## 1.0.0.rc2 (2011-11-11) + +Features: + + - allow passing `display` parameter (@mkdynamic) + - included default scope (@mkdynamic) + +## 1.0.0.rc1 (2011-10-29) + + - first public gem release (@mkdynamic) diff --git a/Gemfile b/Gemfile index b4134c7..08c3acf 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source :rubygems +source 'https://rubygems.org' gemspec diff --git a/README.md b/README.md index 1fc82e4..374d105 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ -# OmniAuth Facebook  [![Build Status](http://travis-ci.org/mkdynamic/omniauth-facebook.png?branch=master)](http://travis-ci.org/mkdynamic/omniauth-facebook) +**NOTE: If you're running < 1.5.1, please upgrade to address 2 security vulnerabilities. +More details [here](https://github.com/mkdynamic/omniauth-facebook/wiki/CSRF-vulnerability:-CVE-2013-4562) and [here](https://github.com/mkdynamic/omniauth-facebook/wiki/Access-token-vulnerability:-CVE-2013-4593).** -Facebook OAuth2 Strategy for OmniAuth 1.0. +--- + +# OmniAuth Facebook  [![Build Status](https://secure.travis-ci.org/mkdynamic/omniauth-facebook.png?branch=master)](https://travis-ci.org/mkdynamic/omniauth-facebook) + +Facebook OAuth2 Strategy for OmniAuth. Supports the OAuth 2.0 server-side and client-side flows. Read the Facebook docs for more details: http://developers.facebook.com/docs/authentication @@ -16,7 +21,7 @@ ## Usage -`OmniAuth::Strategies::Facebook` is simply a Rack middleware. Read the OmniAuth 1.0 docs for detailed instructions: https://github.com/intridea/omniauth. +`OmniAuth::Strategies::Facebook` is simply a Rack middleware. Read the OmniAuth docs for detailed instructions: https://github.com/intridea/omniauth. Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`: @@ -37,7 +42,9 @@ * `auth_type`: Optionally specifies the requested authentication features as a comma-separated list, as per https://developers.facebook.com/docs/authentication/reauthentication/. Valid values are `https` (checks for the presence of the secure cookie and asks for re-authentication if it is not present), and `reauthenticate` (asks the user to re-authenticate unconditionally). Default is `nil`. * `secure_image_url`: Set to `true` to use https for the avatar image url returned in the auth hash. Default is `false`. -* `image_size`: Set the size for the returned image url in the auth hash. Valid options are `square` (50x50), `small` (50 pixels wide, variable height), `normal` (100 pixels wide, variable height), or `large` (about 200 pixels wide, variable height). Default is `square` (50x50). +* `image_size`: Set the size for the returned image url in the auth hash. Valid options include `square` (50x50), `small` (50 pixels wide, variable height), `normal` (100 pixels wide, variable height), or `large` (about 200 pixels wide, variable height). Additionally, you can request a picture of a specific size by setting this option to a hash with `:width` and `:height` as keys. This will return an available profile picture closest to the requested size and requested aspect ratio. If only `:width` or `:height` is specified, we will return a picture whose width or height is closest to the requested size, respectively. +* `info_fields`: Specify exactly which fields should be returned when getting the user's info. Value should be a comma-separated string as per https://developers.facebook.com/docs/reference/api/user/ (only /me endpoint). +* `locale`: Specify locale which should be used when getting the user's info. Value should be locale string as per https://developers.facebook.com/docs/reference/api/locale/. For example, to request `email`, `user_birthday` and `read_stream` permissions and display the authentication page in a popup window: @@ -50,9 +57,7 @@ ### Per-Request Options -If you want to set the `display` format or `scope` on a per-request basis, you can just pass it to the OmniAuth request phase URL, for example: `/auth/facebook?display=popup` or `/auth/facebook?scope=email`. - -You can also pass through a `state` param which will be passed along to the callback url. +If you want to set the `display` format, `auth_type`, or `scope` on a per-request basis, you can just pass it to the OmniAuth request phase URL, for example: `/auth/facebook?display=popup` or `/auth/facebook?scope=email`. ### Custom Callback URL/Path @@ -136,7 +141,7 @@ Take a look at [the example Sinatra app for one option of how you can integrate with a canvas page](https://github.com/mkdynamic/omniauth-facebook/blob/master/example/config.ru). -Bear in mind you have several options (including [authenticated referrals](https://developers.facebook.com/docs/opengraph/authentication/#referrals)). Read [the Facebook docs on canvas page authentication](https://developers.facebook.com/docs/authentication/canvas/) for more info. +Bear in mind you have several [options](https://developers.facebook.com/docs/opengraph/authentication). Read [the Facebook docs on canvas page authentication](https://developers.facebook.com/docs/authentication/canvas/) for more info. ## Token Expiry @@ -150,7 +155,7 @@ ### Server-Side Flow -If you use the server-side flow, Facebook will give you back a longer loved access token (~ 60 days). +If you use the server-side flow, Facebook will give you back a longer lived access token (~ 60 days). If you're having issue getting a long lived token with the server-side flow, make sure to enable the 'deprecate offline_access setting' in you Facebook app config. Read the [Facebook docs about the offline_access deprecation](https://developers.facebook.com/roadmap/offline-access-removal/) for more information. @@ -158,12 +163,13 @@ Actively tested with the following Ruby versions: +- MRI 2.0.0 - MRI 1.9.3 - MRI 1.9.2 - MRI 1.8.7 -- JRuby 1.6.5 +- JRuby 1.7.4 -*NB.* For JRuby, you'll need to install the `jruby-openssl` gem. There's no way to automatically specify this in a Rubygem gemspec, so you need to manually add it your project's own Gemfile: +*NB.* For JRuby < 1.7, you'll need to install the `jruby-openssl` gem. There's no way to automatically specify this in a Rubygem gemspec, so you need to manually add it your project's own Gemfile: ```ruby gem 'jruby-openssl', :platform => :jruby diff --git a/checksums.yaml.gz b/checksums.yaml.gz new file mode 100644 index 0000000..63fad67 Binary files /dev/null and b/checksums.yaml.gz differ diff --git a/example/Gemfile b/example/Gemfile index c37f32d..9962447 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -1,4 +1,4 @@ -source :rubygems +source 'https://rubygems.org' gem 'sinatra' gem 'omniauth-facebook', :path => '../' diff --git a/example/Gemfile.lock b/example/Gemfile.lock index c477ef7..50f5617 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -1,41 +1,40 @@ PATH remote: ../ specs: - omniauth-facebook (1.4.0) - omniauth-oauth2 (~> 1.1.0) + omniauth-facebook (1.6.0.rc1) + omniauth-oauth2 (~> 1.1) GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - faraday (0.8.1) - multipart-post (~> 1.1) - hashie (1.2.0) - httpauth (0.1) - json (1.7.3) - jwt (0.1.4) - json (>= 1.2.4) - multi_json (1.3.6) - multipart-post (1.1.5) - oauth2 (0.8.0) + faraday (0.8.8) + multipart-post (~> 1.2.0) + hashie (2.0.5) + httpauth (0.2.0) + jwt (0.1.8) + multi_json (>= 1.5) + multi_json (1.8.2) + multipart-post (1.2.0) + oauth2 (0.8.1) faraday (~> 0.8) httpauth (~> 0.1) jwt (~> 0.1.4) multi_json (~> 1.0) rack (~> 1.2) - omniauth (1.1.0) - hashie (~> 1.2) + omniauth (1.1.4) + hashie (>= 1.2, < 3) rack - omniauth-oauth2 (1.1.0) + omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) - rack (1.4.1) - rack-protection (1.2.0) + rack (1.5.2) + rack-protection (1.5.1) rack - sinatra (1.3.2) - rack (~> 1.3, >= 1.3.6) - rack-protection (~> 1.2) - tilt (~> 1.3, >= 1.3.3) - tilt (1.3.3) + sinatra (1.4.4) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + tilt (1.4.1) PLATFORMS ruby diff --git a/example/config.ru b/example/config.ru index 47409ca..267f392 100644 --- a/example/config.ru +++ b/example/config.ru @@ -87,15 +87,10 @@ # signed_request FB sends us, asking for auth if the user has # not already granted access, or simply moving straight to the # callback where they have already granted access. - # - # we pass the state parameter which we can detect in our callback - # to do custom rendering/redirection for the canvas app page - redirect "/auth/facebook?signed_request=#{request.params['signed_request']}&state=canvas" + redirect "/auth/facebook?signed_request=#{request.params['signed_request']}" end get '/auth/:provider/callback' do - # we can do something special here is +state+ param is canvas - # (see notes above in /canvas/ method for more details) content_type 'application/json' MultiJson.encode(request.env) end diff --git a/lib/omniauth/facebook/version.rb b/lib/omniauth/facebook/version.rb index 1efaaf2..bf5e6ac 100644 --- a/lib/omniauth/facebook/version.rb +++ b/lib/omniauth/facebook/version.rb @@ -1,5 +1,5 @@ module OmniAuth module Facebook - VERSION = "1.4.1" + VERSION = "1.6.0" end end diff --git a/lib/omniauth/strategies/facebook.rb b/lib/omniauth/strategies/facebook.rb index 92da028..1bd257e 100644 --- a/lib/omniauth/strategies/facebook.rb +++ b/lib/omniauth/strategies/facebook.rb @@ -2,16 +2,19 @@ require 'base64' require 'openssl' require 'rack/utils' +require 'uri' module OmniAuth module Strategies class Facebook < OmniAuth::Strategies::OAuth2 class NoAuthorizationCodeError < StandardError; end + class UnknownSignatureAlgorithmError < NotImplementedError; end DEFAULT_SCOPE = 'email' option :client_options, { :site => 'https://graph.facebook.com', + :authorize_url => "https://www.facebook.com/dialog/oauth", :token_url => '/oauth/access_token' } @@ -35,7 +38,7 @@ 'name' => raw_info['name'], 'first_name' => raw_info['first_name'], 'last_name' => raw_info['last_name'], - 'image' => "#{options[:secure_image_url] ? 'https' : 'http'}://graph.facebook.com/#{uid}/picture?type=#{options[:image_size] || 'square'}", + 'image' => image_url(uid, options), 'description' => raw_info['bio'], 'urls' => { 'Facebook' => raw_info['link'], @@ -53,16 +56,85 @@ end def raw_info - @raw_info ||= access_token.get('/me').parsed || {} - end + @raw_info ||= access_token.get('/me', info_options).parsed || {} + end + + def info_options + params = {:appsecret_proof => appsecret_proof} + params.merge!({:fields => options[:info_fields]}) if options[:info_fields] + params.merge!({:locale => options[:locale]}) if options[:locale] + + { :params => params } + end + + def callback_phase + super + rescue NoAuthorizationCodeError => e + fail!(:no_authorization_code, e) + rescue UnknownSignatureAlgorithmError => e + fail!(:unknown_signature_algoruthm, e) + end + + def request_phase + if signed_request_contains_access_token? + # If we already have an access token, we can just hit the callback URL directly and pass the signed request. + params = { :signed_request => raw_signed_request } + query = Rack::Utils.build_query(params) + + url = callback_url + url << "?" unless url.match(/\?/) + url << "&" unless url.match(/[\&\?]$/) + url << query + + redirect url + else + super + end + end + + # NOTE If we're using code from the signed request then FB sets the redirect_uri to '' during the authorize + # phase and it must match during the access_token phase: + # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348 + def callback_url + if @authorization_code_from_signed_request + '' + else + options[:callback_url] || super + end + end + + def access_token_options + options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h } + end + + # You can pass +display+, +scope+, or +auth_type+ params to the auth request, if you need to set them dynamically. + # You can also set these options in the OmniAuth config :authorize_params option. + # + # /auth/facebook?display=popup + def authorize_params + super.tap do |params| + %w[display scope auth_type].each do |v| + if request.params[v] + params[v.to_sym] = request.params[v] + end + end + + params[:scope] ||= DEFAULT_SCOPE + end + end + + # Parse signed request in order, from: + # + # 1. The request 'signed_request' param (server-side flow from canvas pages) or + # 2. A cookie (client-side flow via JS SDK) + def signed_request + @signed_request ||= raw_signed_request && parse_signed_request(raw_signed_request) + end + + protected def build_access_token - if access_token = request.params["access_token"] - ::OAuth2::AccessToken.from_hash( - client, - {"access_token" => access_token}.update(access_token_options) - ) - elsif signed_request_contains_access_token? + if signed_request_contains_access_token? hash = signed_request.clone ::OAuth2::AccessToken.new( client, @@ -76,99 +148,24 @@ end end - def request_phase - if signed_request_contains_access_token? - # if we already have an access token, we can just hit the - # callback URL directly and pass the signed request along - params = { :signed_request => raw_signed_request } - params[:state] = request.params['state'] if request.params['state'] - query = Rack::Utils.build_query(params) - - url = callback_url - url << "?" unless url.match(/\?/) - url << "&" unless url.match(/[\&\?]$/) - url << query - - redirect url - else - super - end - end - - # NOTE if we're using code from the signed request - # then FB sets the redirect_uri to '' during the authorize - # phase + it must match during the access_token phase: - # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348 - def callback_url - if @authorization_code_from_signed_request - '' - else - options[:callback_url] || super - end - end - - def access_token_options - options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h } - end - - ## - # You can pass +display+, +state+ or +scope+ params to the auth request, if - # you need to set them dynamically. You can also set these options - # in the OmniAuth config :authorize_params option. - # - # /auth/facebook?display=popup&state=ABC - # - def authorize_params - super.tap do |params| - %w[display state scope].each do |v| - if request.params[v] - params[v.to_sym] = request.params[v] - - # to support omniauth-oauth2's auto csrf protection - session['omniauth.state'] = params[:state] if v == 'state' - end - end - - params[:scope] ||= DEFAULT_SCOPE - end - end - - ## - # Parse signed request in order, from: - # - # 1. the request 'signed_request' param (server-side flow from canvas pages) or - # 2. a cookie (client-side flow via JS SDK) - # - def signed_request - @signed_request ||= raw_signed_request && - parse_signed_request(raw_signed_request) - end - private def raw_signed_request - request.params['signed_request'] || - request.cookies["fbsr_#{client.id}"] - end - - ## - # If the signed_request comes from a FB canvas page and the user - # has already authorized your application, the JSON object will be - # contain the access token. + request.params['signed_request'] || request.cookies["fbsr_#{client.id}"] + end + + # If the signed_request comes from a FB canvas page and the user has already authorized your application, the JSON + # object will be contain the access token. # # https://developers.facebook.com/docs/authentication/canvas/ - # def signed_request_contains_access_token? - signed_request && - signed_request['oauth_token'] - end - - ## + signed_request && signed_request['oauth_token'] + end + # Picks the authorization code in order, from: # - # 1. the request 'code' param (manual callback from standard server-side flow) - # 2. a signed request (see #signed_request for more) - # + # 1. The request 'code' param (manual callback from standard server-side flow) + # 2. A signed request (see #signed_request for more) def with_authorization_code! if request.params.key?('code') yield @@ -195,12 +192,13 @@ def parse_signed_request(value) signature, encoded_payload = value.split('.') + return if signature.nil? decoded_hex_signature = base64_decode_url(signature) decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload)) unless decoded_payload['algorithm'] == 'HMAC-SHA256' - raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}" + raise UnknownSignatureAlgorithmError, "unknown algorithm: #{decoded_payload['algorithm']}" end if valid_signature?(client.secret, decoded_hex_signature, encoded_payload) @@ -216,6 +214,24 @@ value += '=' * (4 - value.size.modulo(4)) Base64.decode64(value.tr('-_', '+/')) end + + def image_url(uid, options) + uri_class = options[:secure_image_url] ? URI::HTTPS : URI::HTTP + url = uri_class.build({:host => 'graph.facebook.com', :path => "/#{uid}/picture"}) + + query = if options[:image_size].is_a?(String) + { :type => options[:image_size] } + elsif options[:image_size].is_a?(Hash) + options[:image_size] + end + url.query = Rack::Utils.build_query(query) if query + + url.to_s + end + + def appsecret_proof + @appsecret_proof ||= OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, client.secret, access_token.token) + end end end end diff --git a/metadata.yml b/metadata.yml index 81ac5a4..9a62135 100644 --- a/metadata.yml +++ b/metadata.yml @@ -1,89 +1,83 @@ --- !ruby/object:Gem::Specification name: omniauth-facebook version: !ruby/object:Gem::Version - version: 1.4.1 - prerelease: + version: 1.6.0 platform: ruby authors: - Mark Dodwell +- Josef Šimánek autorequire: bindir: bin cert_chain: [] -date: 2012-07-07 00:00:00.000000000 Z +date: 2014-01-11 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: omniauth-oauth2 requirement: !ruby/object:Gem::Requirement - none: false requirements: - - ~> - !ruby/object:Gem::Version - version: 1.1.0 + version: '1.1' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement - none: false requirements: - - ~> - !ruby/object:Gem::Version - version: 1.1.0 + version: '1.1' - !ruby/object:Gem::Dependency name: minitest requirement: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: mocha requirement: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' description: email: -- mark@mkdynamic.co.uk +- mark@madeofcode.com +- retro@ballgag.cz executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .travis.yml +- CHANGELOG.md - Gemfile - README.md - Rakefile @@ -99,35 +93,29 @@ - test/support/shared_examples.rb - test/test.rb homepage: https://github.com/mkdynamic/omniauth-facebook -licenses: [] +licenses: +- MIT +metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' - segments: - - 0 - hash: 1875274478054024285 required_rubygems_version: !ruby/object:Gem::Requirement - none: false requirements: - - - ! '>=' + - - '>=' - !ruby/object:Gem::Version version: '0' - segments: - - 0 - hash: 1875274478054024285 requirements: [] rubyforge_project: -rubygems_version: 1.8.24 +rubygems_version: 2.0.2 signing_key: -specification_version: 3 -summary: Facebook strategy for OmniAuth +specification_version: 4 +summary: Facebook OAuth2 Strategy for OmniAuth test_files: - test/helper.rb - test/support/shared_examples.rb diff --git a/omniauth-facebook.gemspec b/omniauth-facebook.gemspec index 44fd1ae..d82cea6 100644 --- a/omniauth-facebook.gemspec +++ b/omniauth-facebook.gemspec @@ -5,17 +5,18 @@ Gem::Specification.new do |s| s.name = 'omniauth-facebook' s.version = OmniAuth::Facebook::VERSION - s.authors = ['Mark Dodwell'] - s.email = ['mark@mkdynamic.co.uk'] - s.summary = 'Facebook strategy for OmniAuth' + s.authors = ['Mark Dodwell', 'Josef Šimánek'] + s.email = ['mark@madeofcode.com', 'retro@ballgag.cz'] + s.summary = 'Facebook OAuth2 Strategy for OmniAuth' s.homepage = 'https://github.com/mkdynamic/omniauth-facebook' + s.license = 'MIT' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ['lib'] - s.add_runtime_dependency 'omniauth-oauth2', '~> 1.1.0' + s.add_runtime_dependency 'omniauth-oauth2', '~> 1.1' s.add_development_dependency 'minitest' s.add_development_dependency 'mocha' diff --git a/test/helper.rb b/test/helper.rb index 90f6d5c..e0b1f26 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,6 +1,6 @@ require 'bundler/setup' require 'minitest/autorun' -require 'mocha' +require 'mocha/setup' require 'omniauth/strategies/facebook' OmniAuth.config.test_mode = true @@ -25,7 +25,7 @@ end end -class TestCase < MiniTest::Unit::TestCase +class TestCase < Minitest::Test extend BlockTestHelper include CustomAssertions end @@ -36,6 +36,7 @@ @request.stubs(:params).returns({}) @request.stubs(:cookies).returns({}) @request.stubs(:env).returns({}) + @request.stubs(:ssl?).returns(false) @client_id = '123' @client_secret = '53cr3tz' diff --git a/test/support/shared_examples.rb b/test/support/shared_examples.rb index 053b3b8..dfbf55f 100644 --- a/test/support/shared_examples.rb +++ b/test/support/shared_examples.rb @@ -50,20 +50,20 @@ assert_equal strategy.authorize_params['state'], strategy.session['omniauth.state'] end - test 'should store state in the session when present in authorize params vs. a random one' do + test 'should not store state in the session when present in authorize params vs. a random one' do @options = { :authorize_params => { :state => 'bar' } } refute_empty strategy.authorize_params['state'] - assert_equal 'bar', strategy.authorize_params[:state] + refute_equal 'bar', strategy.authorize_params[:state] refute_empty strategy.session['omniauth.state'] - assert_equal 'bar', strategy.session['omniauth.state'] + refute_equal 'bar', strategy.session['omniauth.state'] end - test 'should store state in the session when present in request params vs. a random one' do + test 'should not store state in the session when present in request params vs. a random one' do @request.stubs(:params).returns({ 'state' => 'foo' }) refute_empty strategy.authorize_params['state'] - assert_equal 'foo', strategy.authorize_params[:state] + refute_equal 'foo', strategy.authorize_params[:state] refute_empty strategy.session['omniauth.state'] - assert_equal 'foo', strategy.session['omniauth.state'] + refute_equal 'foo', strategy.session['omniauth.state'] end end diff --git a/test/test.rb b/test/test.rb index d0456d2..65f0565 100644 --- a/test/test.rb +++ b/test/test.rb @@ -13,7 +13,7 @@ end test 'has correct authorize url' do - assert_equal '/oauth/authorize', strategy.client.options[:authorize_url] + assert_equal 'https://www.facebook.com/dialog/oauth', strategy.client.options[:authorize_url] end test 'has correct token url' do @@ -56,10 +56,10 @@ assert_equal 'touch', strategy.authorize_params[:display] end - test 'includes state parameter from request when present' do - @request.stubs(:params).returns({ 'state' => 'some_state' }) + test 'includes auth_type parameter from request when present' do + @request.stubs(:params).returns({ 'auth_type' => 'reauthenticate' }) assert strategy.authorize_params.is_a?(Hash) - assert_equal 'some_state', strategy.authorize_params[:state] + assert_equal 'reauthenticate', strategy.authorize_params[:auth_type] end test 'overrides default scope with parameter passed from request' do @@ -101,15 +101,28 @@ @options = { :secure_image_url => true } raw_info = { 'name' => 'Fred Smith', 'id' => '321' } strategy.stubs(:raw_info).returns(raw_info) - assert_equal 'https://graph.facebook.com/321/picture?type=square', strategy.info['image'] - end - - test 'returns the image size specified in the `image_size` option' do + assert_equal 'https://graph.facebook.com/321/picture', strategy.info['image'] + end + + test 'returns the image with size specified in the `image_size` option' do @options = { :image_size => 'normal' } raw_info = { 'name' => 'Fred Smith', 'id' => '321' } strategy.stubs(:raw_info).returns(raw_info) assert_equal 'http://graph.facebook.com/321/picture?type=normal', strategy.info['image'] end + + test 'returns the image with width and height specified in the `image_size` option' do + @options = { :image_size => { :width => 123, :height => 987 } } + raw_info = { 'name' => 'Fred Smith', 'id' => '321' } + strategy.stubs(:raw_info).returns(raw_info) + image_url = strategy.info['image'] + path, query = image_url.split("?") + query_params = Hash[*query.split("&").map {|pair| pair.split("=") }.flatten] + + assert_equal 'http://graph.facebook.com/321/picture', path + assert_equal '123', query_params['width'] + assert_equal '987', query_params['height'] + end end class InfoTestOptionalDataPresent < StrategyTestCase @@ -153,9 +166,9 @@ assert_equal 'I am great', strategy.info['description'] end - test 'returns the square format facebook avatar url' do + test 'returns the facebook avatar url' do @raw_info['id'] = '321' - assert_equal 'http://graph.facebook.com/321/picture?type=square', strategy.info['image'] + assert_equal 'http://graph.facebook.com/321/picture', strategy.info['image'] end test 'returns the Facebook link as the Facebook url' do @@ -233,31 +246,58 @@ def setup super @access_token = stub('OAuth2::AccessToken') + @appsecret_proof = 'appsecret_proof' + @options = {:appsecret_proof => @appsecret_proof} end test 'performs a GET to https://graph.facebook.com/me' do + strategy.stubs(:appsecret_proof).returns(@appsecret_proof) strategy.stubs(:access_token).returns(@access_token) - @access_token.expects(:get).with('/me').returns(stub_everything('OAuth2::Response')) + params = {:params => @options} + @access_token.expects(:get).with('/me', params).returns(stub_everything('OAuth2::Response')) + strategy.raw_info + end + + test 'performs a GET to https://graph.facebook.com/me with locale' do + @options.merge!({ :locale => 'cs_CZ' }) + strategy.stubs(:access_token).returns(@access_token) + strategy.stubs(:appsecret_proof).returns(@appsecret_proof) + params = {:params => @options} + @access_token.expects(:get).with('/me', params).returns(stub_everything('OAuth2::Response')) + strategy.raw_info + end + + test 'performs a GET to https://graph.facebook.com/me with info_fields' do + @options.merge!({:info_fields => 'about'}) + strategy.stubs(:access_token).returns(@access_token) + strategy.stubs(:appsecret_proof).returns(@appsecret_proof) + params = {:params => {:appsecret_proof => @appsecret_proof, :fields => 'about'}} + @access_token.expects(:get).with('/me', params).returns(stub_everything('OAuth2::Response')) strategy.raw_info end test 'returns a Hash' do strategy.stubs(:access_token).returns(@access_token) + strategy.stubs(:appsecret_proof).returns(@appsecret_proof) raw_response = stub('Faraday::Response') raw_response.stubs(:body).returns('{ "ohai": "thar" }') raw_response.stubs(:status).returns(200) raw_response.stubs(:headers).returns({'Content-Type' => 'application/json' }) oauth2_response = OAuth2::Response.new(raw_response) - @access_token.stubs(:get).with('/me').returns(oauth2_response) + params = {:params => @options} + @access_token.stubs(:get).with('/me', params).returns(oauth2_response) assert_kind_of Hash, strategy.raw_info assert_equal 'thar', strategy.raw_info['ohai'] end test 'returns an empty hash when the response is false' do strategy.stubs(:access_token).returns(@access_token) + strategy.stubs(:appsecret_proof).returns(@appsecret_proof) oauth2_response = stub('OAuth2::Response', :parsed => false) - @access_token.stubs(:get).with('/me').returns(oauth2_response) + params = {:params => @options} + @access_token.stubs(:get).with('/me', params).returns(oauth2_response) assert_kind_of Hash, strategy.raw_info + assert_equal({}, strategy.raw_info) end test 'should not include raw_info in extras hash when skip_info is specified' do @@ -360,13 +400,18 @@ test 'is nil' do assert_nil strategy.send(:signed_request) end + + test 'throws an error on calling build_access_token' do + assert_equal 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)', + assert_raises(OmniAuth::Strategies::Facebook::NoAuthorizationCodeError) { strategy.send(:build_access_token) }.message + end end class CookiePresentTest < TestCase - def setup - super + def setup(algo = nil) + super() @payload = { - 'algorithm' => 'HMAC-SHA256', + 'algorithm' => algo || 'HMAC-SHA256', 'code' => 'm4c0d3z', 'issued_at' => Time.now.to_i, 'user_id' => '123456' @@ -378,13 +423,18 @@ test 'parses the access code out from the cookie' do assert_equal @payload, strategy.send(:signed_request) end + + test 'throws an error if the algorithm is unknown' do + setup('UNKNOWN-ALGO') + assert_equal "unknown algorithm: UNKNOWN-ALGO", assert_raises(OmniAuth::Strategies::Facebook::UnknownSignatureAlgorithmError) { strategy.send(:signed_request) }.message + end end class ParamPresentTest < TestCase - def setup - super + def setup(algo = nil) + super() @payload = { - 'algorithm' => 'HMAC-SHA256', + 'algorithm' => algo || 'HMAC-SHA256', 'oauth_token' => 'XXX', 'issued_at' => Time.now.to_i, 'user_id' => '123456' @@ -395,6 +445,11 @@ test 'parses the access code out from the param' do assert_equal @payload, strategy.send(:signed_request) + end + + test 'throws an error if the algorithm is unknown' do + setup('UNKNOWN-ALGO') + assert_equal "unknown algorithm: UNKNOWN-ALGO", assert_raises(OmniAuth::Strategies::Facebook::UnknownSignatureAlgorithmError) { strategy.send(:signed_request) }.message end end @@ -420,6 +475,18 @@ assert_equal @payload_from_param, strategy.send(:signed_request) end end + + class EmptySignedRequestTest < TestCase + def setup + super + @request.stubs(:params).returns({'signed_request' => ''}) + end + + test 'empty param' do + assert_equal nil, strategy.send(:signed_request) + end + end + end class RequestPhaseWithSignedRequestTest < StrategyTestCase @@ -465,30 +532,14 @@ end test 'returns a new access token from the signed request' do - result = strategy.build_access_token + result = strategy.send(:build_access_token) assert_kind_of ::OAuth2::AccessToken, result assert_equal @payload['oauth_token'], result.token end test 'returns an access token with the correct expiry time' do - result = strategy.build_access_token + result = strategy.send(:build_access_token) assert_equal @payload['expires'], result.expires_at end end - - class ParamsContainAccessTokenStringTest < TestCase - def setup - super - - @request.stubs(:params).returns({'access_token' => 'm4c0d3z'}) - - strategy.stubs(:callback_url).returns('/') - end - - test 'returns a new access token' do - result = strategy.build_access_token - assert_kind_of ::OAuth2::AccessToken, result - assert_equal 'm4c0d3z', result.token - end - end -end +end