omniauth-hackid 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ .rspec
4
+ /Gemfile.lock
5
+ pkg/*
6
+ .powenv
7
+ tmp
8
+ bin
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby
6
+ branches:
7
+ only:
8
+ - master
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'jruby-openssl', :platform => :jruby
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # OmniAuth HackID
2
+
3
+ HackID OAuth2 Strategy for OmniAuth 1.0.
4
+
5
+ Supports the OAuth 2.0 server-side and client-side flows. Read the HackID docs for more details: https://hackid.herokuapp.com/docs
6
+
7
+ ## Installing
8
+
9
+ Add to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'omniauth-hackid'
13
+ ```
14
+
15
+ Then `bundle install`.
16
+
17
+ ## Usage
18
+
19
+ `OmniAuth::Strategies::HackID` is simply a Rack middleware. Read the OmniAuth 1.0 docs for detailed instructions: https://github.com/intridea/omniauth.
20
+
21
+ Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`:
22
+
23
+ ```ruby
24
+ Rails.application.config.middleware.use OmniAuth::Builder do
25
+ provider :facebook, ENV['HACKID_KEY'], ENV['HACKID_SECRET']
26
+ end
27
+ ```
28
+
29
+ [See the example Sinatra app for full examples](https://github.com/mkdynamic/omniauth-facebook/blob/master/example/config.ru)
30
+
31
+ ## Configuring
32
+
33
+ You can configure several options, which you pass in to the `provider` method via a `Hash`:
34
+
35
+ * `scope`: A comma-separated list of permissions that has not been implemented yet.
36
+
37
+ ### Custom Callback URL/Path
38
+
39
+ You can set a custom `callback_url` or `callback_path` option to override the default value. See [OmniAuth::Strategy#callback_url](https://github.com/intridea/omniauth/blob/master/lib/omniauth/strategy.rb#L411) for more details on the default.
40
+
41
+ ## Auth Hash
42
+
43
+ Here's an example *Auth Hash* available in `request.env['omniauth.auth']`:
44
+
45
+ ```ruby
46
+ {
47
+ :provider => 'facebook',
48
+ :uid => '1234567',
49
+ :info => {
50
+ :nickname => 'jbloggs',
51
+ :email => 'joe@bloggs.com',
52
+ :name => 'Joe Bloggs',
53
+ :first_name => 'Joe',
54
+ :last_name => 'Bloggs',
55
+ :verified => true
56
+ },
57
+ :credentials => {
58
+ :token => 'ABCDEF...', # OAuth 2.0 access_token, which you may wish to store
59
+ :expires_at => 1321747205, # when the access token expires (it always will)
60
+ :expires => true # this will always be true
61
+ },
62
+ :extra => {
63
+ :raw_info => {
64
+ :name => 'Joe Bloggs',
65
+ :email => 'joe@bloggs.com',
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ The precise information available may depend on the permissions which you request.
72
+
73
+ The expiration time of the access token you obtain will depend on which flow you are using. See below for more details.
74
+
75
+ ### Server-Side Flow
76
+
77
+ If you use the server-side flow, HackID will give you back a longer loved access token (~ 14 days).
78
+
79
+ ## Supported Rubies
80
+
81
+ Actively tested with the following Ruby versions:
82
+
83
+ - MRI 1.9.3
84
+ - MRI 1.9.2
85
+ - MRI 1.8.7
86
+ - JRuby 1.6.5
87
+
88
+ *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:
89
+
90
+ ```ruby
91
+ gem 'jruby-openssl', :platform => :jruby
92
+ ```
93
+
94
+ ## License
95
+
96
+ Copyright (c) 2012 by Mark Dodwell
97
+
98
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
101
+
102
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |task|
5
+ task.libs << 'test'
6
+ end
7
+
8
+ task :default => :test
data/example/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem 'sinatra'
4
+ gem 'debugger'
5
+ gem 'omniauth-hackid', :path => '../'
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ omniauth-hackid (0.0.1)
5
+ omniauth-oauth2 (~> 1.1)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ columnize (0.3.6)
11
+ debugger (1.2.0)
12
+ columnize (>= 0.3.1)
13
+ debugger-linecache (~> 1.1.1)
14
+ debugger-ruby_core_source (~> 1.1.3)
15
+ debugger-linecache (1.1.2)
16
+ debugger-ruby_core_source (>= 1.1.1)
17
+ debugger-ruby_core_source (1.1.3)
18
+ faraday (0.8.4)
19
+ multipart-post (~> 1.1)
20
+ hashie (1.2.0)
21
+ httpauth (0.1)
22
+ jwt (0.1.5)
23
+ multi_json (>= 1.0)
24
+ multi_json (1.3.6)
25
+ multipart-post (1.1.5)
26
+ oauth2 (0.8.0)
27
+ faraday (~> 0.8)
28
+ httpauth (~> 0.1)
29
+ jwt (~> 0.1.4)
30
+ multi_json (~> 1.0)
31
+ rack (~> 1.2)
32
+ omniauth (1.1.1)
33
+ hashie (~> 1.2)
34
+ rack
35
+ omniauth-oauth2 (1.1.0)
36
+ oauth2 (~> 0.8.0)
37
+ omniauth (~> 1.0)
38
+ rack (1.4.1)
39
+ rack-protection (1.2.0)
40
+ rack
41
+ sinatra (1.3.2)
42
+ rack (~> 1.3, >= 1.3.6)
43
+ rack-protection (~> 1.2)
44
+ tilt (~> 1.3, >= 1.3.3)
45
+ tilt (1.3.3)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ debugger
52
+ omniauth-hackid!
53
+ sinatra
data/example/config.ru ADDED
@@ -0,0 +1,36 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra/base'
3
+ require 'omniauth-hackid'
4
+
5
+ SCOPE = 'email,read_stream'
6
+
7
+ class App < Sinatra::Base
8
+ # turn off sinatra default X-Frame-Options for FB canvas
9
+ set :protection, :except => :frame_options
10
+
11
+ # server-side flow
12
+ get '/' do
13
+ # NOTE: you would just hit this endpoint directly from the browser
14
+ # in a real app. the redirect is just here to setup the root
15
+ # path in this example sinatra app.
16
+ redirect '/auth/hackid'
17
+ end
18
+
19
+ get '/auth/:provider/callback' do
20
+ content_type 'application/json'
21
+ MultiJson.encode(request.env)
22
+ end
23
+
24
+ get '/auth/failure' do
25
+ content_type 'application/json'
26
+ MultiJson.encode(request.env)
27
+ end
28
+ end
29
+
30
+ use Rack::Session::Cookie
31
+
32
+ use OmniAuth::Builder do
33
+ provider :hackid, ENV['APP_ID'], ENV['APP_SECRET'], :scope => SCOPE
34
+ end
35
+
36
+ run App.new
@@ -0,0 +1 @@
1
+ require 'omniauth/hackid'
@@ -0,0 +1,2 @@
1
+ require 'omniauth/hackid/version'
2
+ require 'omniauth/strategies/hackid'
@@ -0,0 +1,5 @@
1
+ module OmniAuth
2
+ module HackID
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,223 @@
1
+ require 'omniauth/strategies/oauth2'
2
+ require 'base64'
3
+ require 'openssl'
4
+ require 'rack/utils'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ class HackID < OmniAuth::Strategies::OAuth2
9
+ class NoAuthorizationCodeError < StandardError; end
10
+
11
+ DEFAULT_SCOPE = 'email'
12
+
13
+ option :client_options, {
14
+ :site => 'https://hackid.herokuapp.com',
15
+ :token_url => '/oauth/access_token'
16
+ }
17
+
18
+ option :token_params, {
19
+ :parse => :query
20
+ }
21
+
22
+ option :access_token_options, {
23
+ :mode => :query,
24
+ :param_name => 'access_token'
25
+ }
26
+
27
+ option :authorize_options, [:scope, :display, :auth_type]
28
+
29
+ uid { raw_info['id'] }
30
+
31
+ info do
32
+ prune!({
33
+ 'nickname' => raw_info['name'],
34
+ 'email' => raw_info['email'],
35
+ 'name' => raw_info['name'],
36
+ 'first_name' => raw_info['name'].split(" ")[0],
37
+ 'last_name' => raw_info['name'].split(" ")[1],
38
+ 'image' => "",
39
+ #'description' => raw_info['bio'],
40
+ 'urls' => {
41
+ #'Facebook' => raw_info['link'],
42
+ #'Website' => raw_info['website']
43
+ },
44
+ #'location' => (raw_info['location'] || {})['name'],
45
+ 'verified' => raw_info['verified']
46
+ })
47
+ end
48
+
49
+ extra do
50
+ hash = {}
51
+ hash['raw_info'] = raw_info unless skip_info?
52
+ prune! hash
53
+ end
54
+
55
+ def raw_info
56
+ @raw_info ||= access_token.get('/me').parsed || {}
57
+ end
58
+
59
+ def build_access_token
60
+ if access_token = request.params["access_token"]
61
+ ::OAuth2::AccessToken.from_hash(
62
+ client,
63
+ {"access_token" => access_token}.update(access_token_options)
64
+ )
65
+ elsif signed_request_contains_access_token?
66
+ hash = signed_request.clone
67
+ ::OAuth2::AccessToken.new(
68
+ client,
69
+ hash.delete('oauth_token'),
70
+ hash.merge!(access_token_options.merge(:expires_at => hash.delete('expires')))
71
+ )
72
+ else
73
+ with_authorization_code! { super }.tap do |token|
74
+ token.options.merge!(access_token_options)
75
+ end
76
+ end
77
+ end
78
+
79
+ def request_phase
80
+ if signed_request_contains_access_token?
81
+ # if we already have an access token, we can just hit the
82
+ # callback URL directly and pass the signed request along
83
+ params = { :signed_request => raw_signed_request }
84
+ params[:state] = request.params['state'] if request.params['state']
85
+ query = Rack::Utils.build_query(params)
86
+
87
+ url = callback_url
88
+ url << "?" unless url.match(/\?/)
89
+ url << "&" unless url.match(/[\&\?]$/)
90
+ url << query
91
+
92
+ redirect url
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ # NOTE if we're using code from the signed request
99
+ # then HackID sets the redirect_uri to '' during the authorize
100
+ # phase + it must match during the access_token phase:
101
+ # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348
102
+ def callback_url
103
+ if @authorization_code_from_signed_request
104
+ ''
105
+ else
106
+ options[:callback_url] || super
107
+ end
108
+ end
109
+
110
+ def access_token_options
111
+ options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
112
+ end
113
+
114
+ ##
115
+ # You can pass +display+, +state+ or +scope+ params to the auth request, if
116
+ # you need to set them dynamically. You can also set these options
117
+ # in the OmniAuth config :authorize_params option.
118
+ #
119
+ # /auth/hackid?display=popup&state=ABC
120
+ #
121
+ def authorize_params
122
+ super.tap do |params|
123
+ %w[display state scope].each do |v|
124
+ if request.params[v]
125
+ params[v.to_sym] = request.params[v]
126
+
127
+ # to support omniauth-oauth2's auto csrf protection
128
+ session['omniauth.state'] = params[:state] if v == 'state'
129
+ end
130
+ end
131
+
132
+ params[:scope] ||= DEFAULT_SCOPE
133
+ end
134
+ end
135
+
136
+ ##
137
+ # Parse signed request in order, from:
138
+ #
139
+ # 1. the request 'signed_request' param (server-side flow from canvas pages) or
140
+ # 2. a cookie (client-side flow via JS SDK)
141
+ #
142
+ def signed_request
143
+ @signed_request ||= raw_signed_request &&
144
+ parse_signed_request(raw_signed_request)
145
+ end
146
+
147
+ private
148
+
149
+ def raw_signed_request
150
+ request.params['signed_request'] ||
151
+ request.cookies["fbsr_#{client.id}"]
152
+ end
153
+
154
+ ##
155
+ # If the signed_request comes from a FB canvas page and the user
156
+ # has already authorized your application, the JSON object will be
157
+ # contain the access token.
158
+ #
159
+ # https://developers.facebook.com/docs/authentication/canvas/
160
+ #
161
+ def signed_request_contains_access_token?
162
+ signed_request &&
163
+ signed_request['oauth_token']
164
+ end
165
+
166
+ ##
167
+ # Picks the authorization code in order, from:
168
+ #
169
+ # 1. the request 'code' param (manual callback from standard server-side flow)
170
+ # 2. a signed request (see #signed_request for more)
171
+ #
172
+ def with_authorization_code!
173
+ if request.params.key?('code')
174
+ yield
175
+ elsif code_from_signed_request = signed_request && signed_request['code']
176
+ request.params['code'] = code_from_signed_request
177
+ @authorization_code_from_signed_request = true
178
+ begin
179
+ yield
180
+ ensure
181
+ request.params.delete('code')
182
+ @authorization_code_from_signed_request = false
183
+ end
184
+ else
185
+ raise NoAuthorizationCodeError, 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)'
186
+ end
187
+ end
188
+
189
+ def prune!(hash)
190
+ hash.delete_if do |_, value|
191
+ prune!(value) if value.is_a?(Hash)
192
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
193
+ end
194
+ end
195
+
196
+ def parse_signed_request(value)
197
+ signature, encoded_payload = value.split('.')
198
+
199
+ decoded_hex_signature = base64_decode_url(signature)
200
+ decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload))
201
+
202
+ unless decoded_payload['algorithm'] == 'HMAC-SHA256'
203
+ raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}"
204
+ end
205
+
206
+ if valid_signature?(client.secret, decoded_hex_signature, encoded_payload)
207
+ decoded_payload
208
+ end
209
+ end
210
+
211
+ def valid_signature?(secret, signature, payload, algorithm = OpenSSL::Digest::SHA256.new)
212
+ OpenSSL::HMAC.digest(algorithm, secret, payload) == signature
213
+ end
214
+
215
+ def base64_decode_url(value)
216
+ value += '=' * (4 - value.size.modulo(4))
217
+ Base64.decode64(value.tr('-_', '+/'))
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ OmniAuth.config.add_camelization('hackid', 'HackID')