omniauth-stackoverflow 1.0.0
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 +8 -0
- data/.travis.yml +8 -0
- data/Gemfile +5 -0
- data/README.md +9 -0
- data/Rakefile +6 -0
- data/lib/omniauth/stackoverflow/version.rb +5 -0
- data/lib/omniauth/stackoverflow.rb +2 -0
- data/lib/omniauth/strategies/stackoverflow.rb +211 -0
- data/lib/omniauth-stackoverflow.rb +1 -0
- data/omniauth-stackoverflow.gemspec +23 -0
- metadata +105 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# OmniAuth StackOverflow
|
2
|
+
|
3
|
+
StackOverflow OAuth2 Strategy for OmniAuth 1.0.
|
4
|
+
|
5
|
+
All credit due to [Mark Dodwell](https://github.com/mkdynamic), this repository is a hacked up copy of his [Facebook OAuth2 Strategy](https://github.com/mkdynamic/omniauth-facebook). When I get a chance I will put all the specs back in and everything, promise!
|
6
|
+
|
7
|
+
Configuration is a little different to other Omniauth strategies, Stack Exchange use an app id as well, configure it like this (in the Devise initialiser);
|
8
|
+
|
9
|
+
config.omniauth :stackoverflow, '<client_id>', '<oauth secret>', :scope => 'no_expiry', :oauth_key => '<oauth key>'
|
data/Rakefile
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'omniauth/strategies/oauth2'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
module OmniAuth
|
7
|
+
module Strategies
|
8
|
+
class Stackoverflow < OmniAuth::Strategies::OAuth2
|
9
|
+
class NoAuthorizationCodeError < StandardError; end
|
10
|
+
|
11
|
+
attr_accessor :oauth_key
|
12
|
+
|
13
|
+
DEFAULT_SCOPE = 'email'
|
14
|
+
|
15
|
+
option :client_options, {
|
16
|
+
:site => 'https://stackexchange.com',
|
17
|
+
:token_url => '/oauth/access_token'
|
18
|
+
}
|
19
|
+
|
20
|
+
option :token_params, {
|
21
|
+
:parse => :query
|
22
|
+
}
|
23
|
+
|
24
|
+
option :access_token_options, {
|
25
|
+
:header_format => 'OAuth %s',
|
26
|
+
:param_name => 'access_token'
|
27
|
+
}
|
28
|
+
|
29
|
+
option :authorize_options, [:scope, :display, :oauth_key]
|
30
|
+
|
31
|
+
uid { raw_info['user_id'] }
|
32
|
+
|
33
|
+
info do
|
34
|
+
prune!({
|
35
|
+
'id' => raw_info['user_id'],
|
36
|
+
'display_name' => raw_info['display_name'],
|
37
|
+
'email' => "#{raw_info['user_id']}@stackoverflow.com",
|
38
|
+
'image' => raw_info['profile_image']
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
extra do
|
43
|
+
hash = {}
|
44
|
+
hash['raw_info'] = raw_info unless skip_info?
|
45
|
+
prune! hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def raw_info
|
49
|
+
access_token.client.site = "https://api.stackexchange.com"
|
50
|
+
@raw_info ||= access_token.get('/2.0/me', :params => { 'site' => 'stackoverflow', 'access_token' => access_token.token, 'key' => @oauth_key }).parsed["items"].first || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_access_token
|
54
|
+
|
55
|
+
if signed_request_contains_access_token?
|
56
|
+
|
57
|
+
hash = signed_request.clone
|
58
|
+
::OAuth2::AccessToken.new(
|
59
|
+
client,
|
60
|
+
hash.delete('oauth_token'),
|
61
|
+
hash.merge!(access_token_options.merge(:expires_at => hash.delete('expires')))
|
62
|
+
)
|
63
|
+
else
|
64
|
+
with_authorization_code! { super }.tap do |token|
|
65
|
+
token.options.merge!(access_token_options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def request_phase
|
71
|
+
if signed_request_contains_access_token?
|
72
|
+
|
73
|
+
# if we already have an access token, we can just hit the
|
74
|
+
# callback URL directly and pass the signed request along
|
75
|
+
params = { :signed_request => raw_signed_request }
|
76
|
+
params[:state] = request.params['state'] if request.params['state']
|
77
|
+
|
78
|
+
query = Rack::Utils.build_query(params)
|
79
|
+
|
80
|
+
url = callback_url
|
81
|
+
url << "?" unless url.match(/\?/)
|
82
|
+
url << "&" unless url.match(/[\&\?]$/)
|
83
|
+
url << query
|
84
|
+
|
85
|
+
redirect url
|
86
|
+
else
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def callback_phase
|
92
|
+
@oauth_key = authorize_params[:oauth_key]
|
93
|
+
|
94
|
+
super
|
95
|
+
end
|
96
|
+
# NOTE if we're using code from the signed request
|
97
|
+
# then FB sets the redirect_uri to '' during the authorize
|
98
|
+
# phase + it must match during the access_token phase:
|
99
|
+
# https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348
|
100
|
+
def callback_url
|
101
|
+
if @authorization_code_from_signed_request
|
102
|
+
''
|
103
|
+
else
|
104
|
+
options[:callback_url] || super
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def access_token_options
|
109
|
+
options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# You can pass +display+, +state+ or +scope+ params to the auth request, if
|
114
|
+
# you need to set them dynamically. You can also set these options
|
115
|
+
# in the OmniAuth config :authorize_params option.
|
116
|
+
#
|
117
|
+
# /auth/facebook?display=popup&state=ABC
|
118
|
+
#
|
119
|
+
def authorize_params
|
120
|
+
super.tap do |params|
|
121
|
+
%w[display state scope].each { |v| params[v.to_sym] = request.params[v] if request.params[v] }
|
122
|
+
params[:scope] ||= DEFAULT_SCOPE
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Parse signed request in order, from:
|
128
|
+
#
|
129
|
+
# 1. the request 'signed_request' param (server-side flow from canvas pages) or
|
130
|
+
# 2. a cookie (client-side flow via JS SDK)
|
131
|
+
#
|
132
|
+
def signed_request
|
133
|
+
@signed_request ||= raw_signed_request &&
|
134
|
+
parse_signed_request(raw_signed_request)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def raw_signed_request
|
140
|
+
request.params['signed_request'] ||
|
141
|
+
request.cookies["fbsr_#{client.id}"]
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# If the signed_request comes from a FB canvas page and the user
|
146
|
+
# has already authorized your application, the JSON object will be
|
147
|
+
# contain the access token.
|
148
|
+
#
|
149
|
+
# https://developers.facebook.com/docs/authentication/canvas/
|
150
|
+
#
|
151
|
+
def signed_request_contains_access_token?
|
152
|
+
signed_request &&
|
153
|
+
signed_request['oauth_token']
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Picks the authorization code in order, from:
|
158
|
+
#
|
159
|
+
# 1. the request 'code' param (manual callback from standard server-side flow)
|
160
|
+
# 2. a signed request (see #signed_request for more)
|
161
|
+
#
|
162
|
+
def with_authorization_code!
|
163
|
+
if request.params.key?('code')
|
164
|
+
yield
|
165
|
+
elsif code_from_signed_request = signed_request && signed_request['code']
|
166
|
+
request.params['code'] = code_from_signed_request
|
167
|
+
@authorization_code_from_signed_request = true
|
168
|
+
begin
|
169
|
+
yield
|
170
|
+
ensure
|
171
|
+
request.params.delete('code')
|
172
|
+
@authorization_code_from_signed_request = false
|
173
|
+
end
|
174
|
+
else
|
175
|
+
raise NoAuthorizationCodeError, 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def prune!(hash)
|
180
|
+
hash.delete_if do |_, value|
|
181
|
+
prune!(value) if value.is_a?(Hash)
|
182
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def parse_signed_request(value)
|
187
|
+
signature, encoded_payload = value.split('.')
|
188
|
+
|
189
|
+
decoded_hex_signature = base64_decode_url(signature)
|
190
|
+
decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload))
|
191
|
+
|
192
|
+
unless decoded_payload['algorithm'] == 'HMAC-SHA256'
|
193
|
+
raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}"
|
194
|
+
end
|
195
|
+
|
196
|
+
if valid_signature?(client.secret, decoded_hex_signature, encoded_payload)
|
197
|
+
decoded_payload
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def valid_signature?(secret, signature, payload, algorithm = OpenSSL::Digest::SHA256.new)
|
202
|
+
OpenSSL::HMAC.digest(algorithm, secret, payload) == signature
|
203
|
+
end
|
204
|
+
|
205
|
+
def base64_decode_url(value)
|
206
|
+
value += '=' * (4 - value.size.modulo(4))
|
207
|
+
Base64.decode64(value.tr('-_', '+/'))
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'omniauth/stackoverflow'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'omniauth/stackoverflow/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'omniauth-stackoverflow'
|
7
|
+
s.version = OmniAuth::Stackoverflow::VERSION
|
8
|
+
s.authors = ['Mark Dodwell', 'Dan Higham']
|
9
|
+
s.email = ['mark@mkdynamic.co.uk', 'dhigham@vmware.com']
|
10
|
+
s.summary = 'StackOverflow strategy for OmniAuth'
|
11
|
+
s.homepage = 'https://github.com/danhigham/omniauth-stackoverflow'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
16
|
+
s.require_paths = ['lib']
|
17
|
+
|
18
|
+
s.add_runtime_dependency 'omniauth-oauth2', '~> 1.0.2'
|
19
|
+
|
20
|
+
s.add_development_dependency 'rspec', '~> 2'
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omniauth-stackoverflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mark Dodwell
|
9
|
+
- Dan Higham
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-06-25 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: omniauth-oauth2
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.0.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 1.0.2
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rspec
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '2'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
description:
|
64
|
+
email:
|
65
|
+
- mark@mkdynamic.co.uk
|
66
|
+
- dhigham@vmware.com
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- .travis.yml
|
73
|
+
- Gemfile
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- lib/omniauth-stackoverflow.rb
|
77
|
+
- lib/omniauth/stackoverflow.rb
|
78
|
+
- lib/omniauth/stackoverflow/version.rb
|
79
|
+
- lib/omniauth/strategies/stackoverflow.rb
|
80
|
+
- omniauth-stackoverflow.gemspec
|
81
|
+
homepage: https://github.com/danhigham/omniauth-stackoverflow
|
82
|
+
licenses: []
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.24
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: StackOverflow strategy for OmniAuth
|
105
|
+
test_files: []
|