omniauth 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of omniauth might be problematic. Click here for more details.

Files changed (197) hide show
  1. data/.autotest +9 -0
  2. data/.document +5 -0
  3. data/.gitignore +36 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +7 -0
  6. data/.yardopts +4 -0
  7. data/Gemfile +6 -1
  8. data/README.markdown +121 -0
  9. data/Rakefile +50 -2
  10. data/lib/omniauth/version.rb +18 -4
  11. data/oa-basic/.gemtest +0 -0
  12. data/oa-basic/.rspec +3 -0
  13. data/oa-basic/.yardopts +4 -0
  14. data/oa-basic/LICENSE +19 -0
  15. data/oa-basic/README.rdoc +34 -0
  16. data/oa-basic/Rakefile +6 -0
  17. data/oa-basic/lib/oa-basic.rb +1 -0
  18. data/oa-basic/lib/omniauth/basic.rb +8 -0
  19. data/oa-basic/lib/omniauth/strategies/http_basic.rb +56 -0
  20. data/oa-basic/lib/omniauth/version.rb +19 -0
  21. data/oa-basic/oa-basic.gemspec +28 -0
  22. data/oa-basic/spec/omniauth/strategies/basic_spec.rb +7 -0
  23. data/oa-basic/spec/spec_helper.rb +11 -0
  24. data/oa-core/.gemtest +0 -0
  25. data/oa-core/.rspec +3 -0
  26. data/oa-core/.yardopts +4 -0
  27. data/oa-core/LICENSE +19 -0
  28. data/oa-core/Rakefile +6 -0
  29. data/oa-core/autotest/discover.rb +1 -0
  30. data/oa-core/lib/oa-core.rb +1 -0
  31. data/oa-core/lib/omniauth/builder.rb +33 -0
  32. data/oa-core/lib/omniauth/core.rb +135 -0
  33. data/oa-core/lib/omniauth/form.rb +184 -0
  34. data/oa-core/lib/omniauth/strategy.rb +227 -0
  35. data/oa-core/lib/omniauth/test.rb +12 -0
  36. data/oa-core/lib/omniauth/test/phony_session.rb +8 -0
  37. data/oa-core/lib/omniauth/test/strategy_macros.rb +34 -0
  38. data/oa-core/lib/omniauth/test/strategy_test_case.rb +49 -0
  39. data/oa-core/lib/omniauth/version.rb +19 -0
  40. data/oa-core/oa-core.gemspec +24 -0
  41. data/oa-core/spec/omniauth/builder_spec.rb +20 -0
  42. data/oa-core/spec/omniauth/core_spec.rb +79 -0
  43. data/oa-core/spec/omniauth/strategy_spec.rb +358 -0
  44. data/oa-core/spec/spec_helper.rb +12 -0
  45. data/oa-enterprise/.gemtest +0 -0
  46. data/oa-enterprise/.rspec +3 -0
  47. data/oa-enterprise/.yardopts +4 -0
  48. data/oa-enterprise/LICENSE +19 -0
  49. data/oa-enterprise/README.rdoc +82 -0
  50. data/oa-enterprise/Rakefile +6 -0
  51. data/oa-enterprise/lib/oa-enterprise.rb +1 -0
  52. data/oa-enterprise/lib/omniauth/enterprise.rb +8 -0
  53. data/oa-enterprise/lib/omniauth/strategies/cas.rb +47 -0
  54. data/oa-enterprise/lib/omniauth/strategies/cas/configuration.rb +98 -0
  55. data/oa-enterprise/lib/omniauth/strategies/cas/service_ticket_validator.rb +84 -0
  56. data/oa-enterprise/lib/omniauth/strategies/ldap.rb +111 -0
  57. data/oa-enterprise/lib/omniauth/strategies/ldap/adaptor.rb +279 -0
  58. data/oa-enterprise/lib/omniauth/version.rb +19 -0
  59. data/oa-enterprise/oa-enterprise.gemspec +32 -0
  60. data/oa-enterprise/spec/fixtures/cas_failure.xml +4 -0
  61. data/oa-enterprise/spec/fixtures/cas_success.xml +8 -0
  62. data/oa-enterprise/spec/omniauth/strategies/cas_spec.rb +94 -0
  63. data/oa-enterprise/spec/spec_helper.rb +14 -0
  64. data/oa-more/.gemtest +0 -0
  65. data/oa-more/.rspec +3 -0
  66. data/oa-more/.yardopts +4 -0
  67. data/oa-more/LICENSE +19 -0
  68. data/oa-more/README.rdoc +22 -0
  69. data/oa-more/Rakefile +6 -0
  70. data/oa-more/lib/oa-more.rb +1 -0
  71. data/oa-more/lib/omniauth/more.rb +9 -0
  72. data/oa-more/lib/omniauth/strategies/flickr.rb +86 -0
  73. data/oa-more/lib/omniauth/strategies/windows_live.rb +39 -0
  74. data/oa-more/lib/omniauth/strategies/windows_live/windowslivelogin.rb +1143 -0
  75. data/oa-more/lib/omniauth/strategies/yupoo.rb +67 -0
  76. data/oa-more/lib/omniauth/version.rb +19 -0
  77. data/oa-more/oa-more.gemspec +30 -0
  78. data/oa-more/spec/omniauth/strategies/flickr_spec.rb +7 -0
  79. data/oa-more/spec/spec_helper.rb +11 -0
  80. data/oa-oauth/.gemtest +0 -0
  81. data/oa-oauth/.rspec +3 -0
  82. data/oa-oauth/.yardopts +4 -0
  83. data/oa-oauth/LICENSE +19 -0
  84. data/oa-oauth/README.rdoc +35 -0
  85. data/oa-oauth/Rakefile +6 -0
  86. data/oa-oauth/autotest/discover.rb +1 -0
  87. data/oa-oauth/lib/oa-oauth.rb +1 -0
  88. data/oa-oauth/lib/omniauth/oauth.rb +53 -0
  89. data/oa-oauth/lib/omniauth/strategies/bitly.rb +46 -0
  90. data/oa-oauth/lib/omniauth/strategies/dailymile.rb +64 -0
  91. data/oa-oauth/lib/omniauth/strategies/doit.rb +60 -0
  92. data/oa-oauth/lib/omniauth/strategies/dopplr.rb +53 -0
  93. data/oa-oauth/lib/omniauth/strategies/douban.rb +60 -0
  94. data/oa-oauth/lib/omniauth/strategies/evernote.rb +54 -0
  95. data/oa-oauth/lib/omniauth/strategies/facebook.rb +70 -0
  96. data/oa-oauth/lib/omniauth/strategies/foursquare.rb +62 -0
  97. data/oa-oauth/lib/omniauth/strategies/github.rb +50 -0
  98. data/oa-oauth/lib/omniauth/strategies/goodreads.rb +44 -0
  99. data/oa-oauth/lib/omniauth/strategies/google.rb +80 -0
  100. data/oa-oauth/lib/omniauth/strategies/gowalla.rb +64 -0
  101. data/oa-oauth/lib/omniauth/strategies/hyves.rb +67 -0
  102. data/oa-oauth/lib/omniauth/strategies/identica.rb +49 -0
  103. data/oa-oauth/lib/omniauth/strategies/instagram.rb +56 -0
  104. data/oa-oauth/lib/omniauth/strategies/instapaper.rb +40 -0
  105. data/oa-oauth/lib/omniauth/strategies/linked_in.rb +55 -0
  106. data/oa-oauth/lib/omniauth/strategies/meetup.rb +56 -0
  107. data/oa-oauth/lib/omniauth/strategies/miso.rb +41 -0
  108. data/oa-oauth/lib/omniauth/strategies/mixi.rb +59 -0
  109. data/oa-oauth/lib/omniauth/strategies/netflix.rb +65 -0
  110. data/oa-oauth/lib/omniauth/strategies/oauth.rb +85 -0
  111. data/oa-oauth/lib/omniauth/strategies/oauth2.rb +98 -0
  112. data/oa-oauth/lib/omniauth/strategies/qzone.rb +69 -0
  113. data/oa-oauth/lib/omniauth/strategies/rdio.rb +45 -0
  114. data/oa-oauth/lib/omniauth/strategies/renren.rb +87 -0
  115. data/oa-oauth/lib/omniauth/strategies/salesforce.rb +44 -0
  116. data/oa-oauth/lib/omniauth/strategies/smug_mug.rb +42 -0
  117. data/oa-oauth/lib/omniauth/strategies/sound_cloud.rb +46 -0
  118. data/oa-oauth/lib/omniauth/strategies/t163.rb +57 -0
  119. data/oa-oauth/lib/omniauth/strategies/teambox.rb +49 -0
  120. data/oa-oauth/lib/omniauth/strategies/thirty_seven_signals.rb +41 -0
  121. data/oa-oauth/lib/omniauth/strategies/tqq.rb +64 -0
  122. data/oa-oauth/lib/omniauth/strategies/trade_me.rb +45 -0
  123. data/oa-oauth/lib/omniauth/strategies/trip_it.rb +22 -0
  124. data/oa-oauth/lib/omniauth/strategies/tsina.rb +79 -0
  125. data/oa-oauth/lib/omniauth/strategies/tsohu.rb +57 -0
  126. data/oa-oauth/lib/omniauth/strategies/tumblr.rb +60 -0
  127. data/oa-oauth/lib/omniauth/strategies/twitter.rb +57 -0
  128. data/oa-oauth/lib/omniauth/strategies/type_pad.rb +76 -0
  129. data/oa-oauth/lib/omniauth/strategies/vimeo.rb +54 -0
  130. data/oa-oauth/lib/omniauth/strategies/vkontakte.rb +84 -0
  131. data/oa-oauth/lib/omniauth/strategies/xauth.rb +67 -0
  132. data/oa-oauth/lib/omniauth/strategies/yahoo.rb +55 -0
  133. data/oa-oauth/lib/omniauth/strategies/yammer.rb +43 -0
  134. data/oa-oauth/lib/omniauth/strategies/you_tube.rb +73 -0
  135. data/oa-oauth/lib/omniauth/version.rb +19 -0
  136. data/oa-oauth/oa-oauth.gemspec +33 -0
  137. data/oa-oauth/spec/fixtures/basecamp_200.xml +24 -0
  138. data/oa-oauth/spec/fixtures/campfire_200.json +10 -0
  139. data/oa-oauth/spec/omniauth/strategies/bitly_spec.rb +5 -0
  140. data/oa-oauth/spec/omniauth/strategies/dailymile_spec.rb +5 -0
  141. data/oa-oauth/spec/omniauth/strategies/doit_spec.rb +5 -0
  142. data/oa-oauth/spec/omniauth/strategies/dopplr_spec.rb +5 -0
  143. data/oa-oauth/spec/omniauth/strategies/douban_spec.rb +5 -0
  144. data/oa-oauth/spec/omniauth/strategies/evernote_spec.rb +5 -0
  145. data/oa-oauth/spec/omniauth/strategies/facebook_spec.rb +5 -0
  146. data/oa-oauth/spec/omniauth/strategies/foursquare_spec.rb +18 -0
  147. data/oa-oauth/spec/omniauth/strategies/github_spec.rb +5 -0
  148. data/oa-oauth/spec/omniauth/strategies/goodreads_spec.rb +6 -0
  149. data/oa-oauth/spec/omniauth/strategies/google_spec.rb +5 -0
  150. data/oa-oauth/spec/omniauth/strategies/gowalla_spec.rb +5 -0
  151. data/oa-oauth/spec/omniauth/strategies/hyves_spec.rb +5 -0
  152. data/oa-oauth/spec/omniauth/strategies/identica_spec.rb +5 -0
  153. data/oa-oauth/spec/omniauth/strategies/linked_in_spec.rb +5 -0
  154. data/oa-oauth/spec/omniauth/strategies/meetup_spec.rb +14 -0
  155. data/oa-oauth/spec/omniauth/strategies/miso_spec.rb +5 -0
  156. data/oa-oauth/spec/omniauth/strategies/netflix_spec.rb +5 -0
  157. data/oa-oauth/spec/omniauth/strategies/oauth2_spec.rb +0 -0
  158. data/oa-oauth/spec/omniauth/strategies/oauth_spec.rb +77 -0
  159. data/oa-oauth/spec/omniauth/strategies/rdio_spec.rb +5 -0
  160. data/oa-oauth/spec/omniauth/strategies/salesforce_spec.rb +5 -0
  161. data/oa-oauth/spec/omniauth/strategies/smug_mug_spec.rb +5 -0
  162. data/oa-oauth/spec/omniauth/strategies/sound_cloud_spec.rb +5 -0
  163. data/oa-oauth/spec/omniauth/strategies/t163_spec.rb +5 -0
  164. data/oa-oauth/spec/omniauth/strategies/teambox_spec.rb +5 -0
  165. data/oa-oauth/spec/omniauth/strategies/thirty_seven_signals_spec.rb +5 -0
  166. data/oa-oauth/spec/omniauth/strategies/trade_me_spec.rb +5 -0
  167. data/oa-oauth/spec/omniauth/strategies/trip_it_spec.rb +5 -0
  168. data/oa-oauth/spec/omniauth/strategies/tsina_spec.rb +5 -0
  169. data/oa-oauth/spec/omniauth/strategies/tumblr_spec.rb +5 -0
  170. data/oa-oauth/spec/omniauth/strategies/twitter_spec.rb +20 -0
  171. data/oa-oauth/spec/omniauth/strategies/type_pad_spec.rb +5 -0
  172. data/oa-oauth/spec/omniauth/strategies/vimeo_spec.rb +5 -0
  173. data/oa-oauth/spec/omniauth/strategies/vkontakte_spec.rb +5 -0
  174. data/oa-oauth/spec/omniauth/strategies/yahoo_spec.rb +5 -0
  175. data/oa-oauth/spec/omniauth/strategies/yammer_spec.rb +5 -0
  176. data/oa-oauth/spec/omniauth/strategies/you_tube_spec.rb +5 -0
  177. data/oa-oauth/spec/spec_helper.rb +27 -0
  178. data/oa-oauth/spec/support/shared_examples.rb +29 -0
  179. data/oa-openid/.gemtest +0 -0
  180. data/oa-openid/.rspec +3 -0
  181. data/oa-openid/.yardopts +4 -0
  182. data/oa-openid/LICENSE +19 -0
  183. data/oa-openid/README.rdoc +51 -0
  184. data/oa-openid/Rakefile +6 -0
  185. data/oa-openid/lib/oa-openid.rb +1 -0
  186. data/oa-openid/lib/omniauth/openid.rb +59 -0
  187. data/oa-openid/lib/omniauth/openid/gapps.rb +32 -0
  188. data/oa-openid/lib/omniauth/strategies/google_apps.rb +23 -0
  189. data/oa-openid/lib/omniauth/strategies/open_id.rb +132 -0
  190. data/oa-openid/lib/omniauth/version.rb +19 -0
  191. data/oa-openid/oa-openid.gemspec +29 -0
  192. data/oa-openid/spec/omniauth/strategies/open_id_spec.rb +71 -0
  193. data/oa-openid/spec/spec_helper.rb +14 -0
  194. data/omniauth.gemspec +3 -3
  195. data/tasks/all.rb +134 -0
  196. metadata +199 -9
  197. data/README.rdoc +0 -17
@@ -0,0 +1,19 @@
1
+ module OmniAuth
2
+ module Version
3
+ unless defined?(::OmniAuth::Version::MAJOR)
4
+ MAJOR = 0
5
+ end
6
+ unless defined?(::OmniAuth::Version::MINOR)
7
+ MINOR = 2
8
+ end
9
+ unless defined?(::OmniAuth::Version::PATCH)
10
+ PATCH = 5
11
+ end
12
+ unless defined?(::OmniAuth::Version::PRE)
13
+ PRE = nil
14
+ end
15
+ unless defined?(::OmniAuth::Version::STRING)
16
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/omniauth/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.add_runtime_dependency 'addressable', '2.2.4'
6
+ gem.add_runtime_dependency 'jruby-openssl', '~> 0.7.3' if RUBY_PLATFORM == 'java'
7
+ gem.add_runtime_dependency 'nokogiri', '~> 1.4.2'
8
+ gem.add_runtime_dependency 'net-ldap', '~> 0.2.2'
9
+ gem.add_runtime_dependency 'oa-core', OmniAuth::Version::STRING
10
+ gem.add_runtime_dependency 'pyu-ruby-sasl', '~> 0.0.3.1'
11
+ gem.add_runtime_dependency 'rubyntlm', '~> 0.1.1'
12
+ gem.add_development_dependency 'maruku', '~> 0.6'
13
+ gem.add_development_dependency 'simplecov', '~> 0.4'
14
+ gem.add_development_dependency 'rack-test', '~> 0.5'
15
+ gem.add_development_dependency 'rake', '~> 0.8'
16
+ gem.add_development_dependency 'rspec', '~> 2.5'
17
+ gem.add_development_dependency 'webmock', '~> 1.6'
18
+ gem.add_development_dependency 'yard', '~> 0.6'
19
+ gem.add_development_dependency 'ZenTest', '~> 4.5'
20
+ gem.name = 'oa-enterprise'
21
+ gem.version = OmniAuth::Version::STRING
22
+ gem.description = %q{Enterprise strategies for OmniAuth.}
23
+ gem.summary = gem.description
24
+ gem.email = ['james.a.rosen@gmail.com', 'ping@intridea.com', 'michael@intridea.com', 'sferik@gmail.com']
25
+ gem.homepage = 'http://github.com/intridea/omniauth'
26
+ gem.authors = ['James A. Rosen', 'Ping Yu', 'Michael Bleigh', 'Erik Michaels-Ober']
27
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
28
+ gem.files = `git ls-files`.split("\n")
29
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ gem.require_paths = ['lib']
31
+ gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if gem.respond_to? :required_rubygems_version=
32
+ end
@@ -0,0 +1,4 @@
1
+ <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
2
+ <cas:authenticationFailure>
3
+ </cas:authenticationFailure>
4
+ </cas:serviceResponse>
@@ -0,0 +1,8 @@
1
+ <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
2
+ <cas:authenticationSuccess>
3
+ <cas:user>psegel</cas:user>
4
+ <cas:first-name>Peter</cas:first-name>
5
+ <cas:last-name>Segel</cas:last-name>
6
+ <hire-date>2004-07-13</hire-date>
7
+ </cas:authenticationSuccess>
8
+ </cas:serviceResponse>
@@ -0,0 +1,94 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+ require 'cgi'
3
+
4
+ describe OmniAuth::Strategies::CAS, :type => :strategy do
5
+
6
+ include OmniAuth::Test::StrategyTestCase
7
+
8
+ def strategy
9
+ @cas_server ||= 'https://cas.example.org'
10
+ [OmniAuth::Strategies::CAS, {:cas_server => @cas_server}]
11
+ end
12
+
13
+ describe 'GET /auth/cas' do
14
+ before do
15
+ get '/auth/cas'
16
+ end
17
+
18
+ it 'should redirect to the CAS server' do
19
+ last_response.should be_redirect
20
+ return_to = CGI.escape(last_request.url + '/callback')
21
+ last_response.headers['Location'].should == @cas_server + '/login?service=' + return_to
22
+ end
23
+ end
24
+
25
+ describe 'GET /auth/cas/callback without a ticket' do
26
+ before do
27
+ get '/auth/cas/callback'
28
+ end
29
+ it 'should fail' do
30
+ last_response.should be_redirect
31
+ last_response.headers['Location'].should =~ /no_ticket/
32
+ end
33
+ end
34
+
35
+ describe 'GET /auth/cas/callback with an invalid ticket' do
36
+ before do
37
+ stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=9391d/).
38
+ to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_failure.xml')))
39
+ get '/auth/cas/callback?ticket=9391d'
40
+ end
41
+ it 'should fail' do
42
+ last_response.should be_redirect
43
+ last_response.headers['Location'].should =~ /invalid_ticket/
44
+ end
45
+ end
46
+
47
+ describe 'GET /auth/cas/callback with a valid ticket' do
48
+ before do
49
+ stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=593af/).
50
+ with { |request| @request_uri = request.uri.to_s }.
51
+ to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_success.xml')))
52
+ get '/auth/cas/callback?ticket=593af'
53
+ end
54
+
55
+ it 'should strip the ticket parameter from the callback URL before sending it to the CAS server' do
56
+ @request_uri.scan('ticket=').length.should == 1
57
+ end
58
+
59
+ sets_an_auth_hash
60
+ sets_provider_to 'cas'
61
+ sets_uid_to 'psegel'
62
+
63
+ it 'should set additional user information' do
64
+ extra = (last_request.env['omniauth.auth'] || {})['extra']
65
+ extra.should be_kind_of(Hash)
66
+ extra['first-name'].should == 'Peter'
67
+ extra['last-name'].should == 'Segel'
68
+ extra['hire-date'].should == '2004-07-13'
69
+ end
70
+
71
+ it 'should call through to the master app' do
72
+ last_response.body.should == 'true'
73
+ end
74
+ end
75
+
76
+ unless RUBY_VERSION =~ /^1\.8\.\d$/
77
+ describe 'GET /auth/cas/callback with a valid ticket and gzipped response from the server on ruby >1.8' do
78
+ before do
79
+ zipped = StringIO.new
80
+ Zlib::GzipWriter.wrap zipped do |io|
81
+ io.write File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_success.xml'))
82
+ end
83
+ stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=593af/).
84
+ with { |request| @request_uri = request.uri.to_s }.
85
+ to_return(:body => zipped.string, :headers => { 'content-encoding' => 'gzip' })
86
+ get '/auth/cas/callback?ticket=593af'
87
+ end
88
+
89
+ it 'should call through to the master app when response is gzipped' do
90
+ last_response.body.should == 'true'
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,14 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+ require 'rspec'
4
+ require 'rack/test'
5
+ require 'webmock/rspec'
6
+ require 'omniauth/core'
7
+ require 'omniauth/test'
8
+ require 'omniauth/enterprise'
9
+
10
+ RSpec.configure do |config|
11
+ config.include WebMock::API
12
+ config.include Rack::Test::Methods
13
+ config.extend OmniAuth::Test::StrategyMacros, :type => :strategy
14
+ end
data/oa-more/.gemtest ADDED
File without changes
data/oa-more/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format=nested
3
+ --backtrace
data/oa-more/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --markup markdown
2
+ --markup-provider maruku
3
+ -
4
+ LICENSE
data/oa-more/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010-2011 Michael Bleigh and Intridea, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ = OmniAuth::More
2
+
3
+ OmniAuth stratgies for authentication providers that do not
4
+ fit into one of the other authentication gems.
5
+
6
+ == Installation
7
+
8
+ To install omniauth as a suite of gems:
9
+
10
+ gem install omniauth
11
+
12
+ To install just the providers in the "more" gem:
13
+
14
+ gem install oa-more
15
+
16
+ == OmniAuth Builder
17
+
18
+ If you want to allow multiple providers, use the OmniAuth Builder:
19
+
20
+ use OmniAuth::Builder do
21
+ provider :flickr, 'api_key', 'secret_key', :scope => 'read'
22
+ end
data/oa-more/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
6
+ task :test => :spec
@@ -0,0 +1 @@
1
+ require 'omniauth/more'
@@ -0,0 +1,9 @@
1
+ require 'omniauth/core'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ autoload :WindowsLive, 'omniauth/strategies/windows_live'
6
+ autoload :Flickr, 'omniauth/strategies/flickr'
7
+ autoload :Yupoo, 'omniauth/strategies/yupoo'
8
+ end
9
+ end
@@ -0,0 +1,86 @@
1
+ require 'omniauth/core'
2
+ require 'digest/md5'
3
+ require 'rest-client'
4
+ require 'multi_json'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ #
9
+ # Authenticate to Flickr
10
+ #
11
+ # @example Basic Usage
12
+ #
13
+ # use OmniAuth::Strategies::Flickr, 'API Key', 'Secret Key', :scope => 'read'
14
+ class Flickr
15
+ include OmniAuth::Strategy
16
+ attr_accessor :api_key, :secret_key, :options
17
+
18
+ # error catching, based on OAuth2 callback
19
+ class CallbackError < StandardError
20
+ attr_accessor :error, :error_reason
21
+ def initialize(error, error_reason)
22
+ self.error = error
23
+ self.error_reason = error_reason
24
+ end
25
+ end
26
+
27
+ # @param [Rack Application] app standard middleware application parameter
28
+ # @param [String] api_key the application id as [registered on Flickr](http://www.flickr.com/services/apps/)
29
+ # @param [String] secret_key the application secret as [registered on Flickr](http://www.flickr.com/services/apps/)
30
+ # @option options ['read','write','delete] :scope ('read') the scope of your authorization request; must be `read` or 'write' or 'delete'
31
+ def initialize(app, api_key, secret_key, options = {})
32
+ super(app, :flickr)
33
+ @api_key = api_key
34
+ @secret_key = secret_key
35
+ @options = {:scope => 'read'}.merge(options)
36
+ end
37
+
38
+ protected
39
+
40
+ def request_phase
41
+ params = { :api_key => api_key, :perms => options[:scope] }
42
+ params[:api_sig] = flickr_sign(params)
43
+ query_string = params.collect{ |key,value| "#{key}=#{Rack::Utils.escape(value)}" }.join('&')
44
+ redirect "http://flickr.com/services/auth/?#{query_string}"
45
+ end
46
+
47
+ def callback_phase
48
+ params = { :api_key => api_key, :method => 'flickr.auth.getToken', :frob => request.params['frob'], :format => 'json', :nojsoncallback => '1' }
49
+ params[:api_sig] = flickr_sign(params)
50
+
51
+ response = RestClient.get('http://api.flickr.com/services/rest/', { :params => params })
52
+ auth = MultiJson.decode(response.to_s)
53
+ raise CallbackError.new(auth['code'],auth['message']) if auth['stat'] == 'fail'
54
+
55
+ @user = auth['auth']['user']
56
+ @access_token = auth['auth']['token']['_content']
57
+
58
+ super
59
+ rescue CallbackError => e
60
+ fail!(:invalid_response, e)
61
+ end
62
+
63
+ def auth_hash
64
+ OmniAuth::Utils.deep_merge(super, {
65
+ 'uid' => @user['nsid'],
66
+ 'credentials' => { 'token' => @access_token },
67
+ 'user_info' => user_info,
68
+ 'extra' => { 'user_hash' => @user }
69
+ })
70
+ end
71
+
72
+ def user_info
73
+ name = @user['fullname']
74
+ name = @user['username'] if name.nil? || name.empty?
75
+ {
76
+ 'nickname' => @user['username'],
77
+ 'name' => name,
78
+ }
79
+ end
80
+
81
+ def flickr_sign(params)
82
+ Digest::MD5.hexdigest(secret_key + params.sort{|a,b| a[0].to_s <=> b[0].to_s }.flatten.join)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ require 'omniauth/core'
2
+ require 'omniauth/strategies/windows_live/windowslivelogin'
3
+
4
+ module OmniAuth
5
+ module Strategies
6
+ class WindowsLive
7
+ include OmniAuth::Strategy
8
+
9
+ attr_accessor :app_id, :app_secret
10
+
11
+ # Initialize the strategy by providing
12
+ #
13
+ # @param app_id [String] The application ID from your registered app with Microsoft.
14
+ # @param app_secret [String] The secret from your registered app with Microsoft.
15
+ # @option options [String] :locale A localization string for the login, should be in the form `en-us` or similar.
16
+ # @option options [String] :state Some state information that is serialized into the query string upon callback.
17
+ # @option options [Boolean] :ssl Whether or not to use SSL for login. Defaults to `true`.
18
+ # @option options [Boolean] :force_nonprovisioned When true, forces a non-provisioned (i.e. no app id or secret) mode.
19
+ def initialize(app, app_id = nil, app_secret = nil, options = {})
20
+ self.app_id = app_id
21
+ self.app_secret = app_secret
22
+ super(app, :windows_live, app_id, app_secret, options)
23
+ options[:ssl] ||= true
24
+ options[:locale] ||= 'en-us'
25
+ options[:force_nonprovisioned] = true unless app_id
26
+ end
27
+
28
+ protected
29
+
30
+ def consumer
31
+ WindowsLiveLogin.new app_id, app_secret, options[:security_algorithm], options[:force_nonprovisioned], options[:policy_url], callback_url
32
+ end
33
+
34
+ def request_phase
35
+ redirect consumer.getLoginUrl(options[:state], options[:locale])
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,1143 @@
1
+ #######################################################################
2
+ # FILE: windowslivelogin.rb
3
+ #
4
+ # DESCRIPTION: Sample implementation of Web Authentication and
5
+ # Delegated Authentication protocol in Ruby. Also
6
+ # includes trusted sign-in and application verification
7
+ # sample implementations.
8
+ #
9
+ # VERSION: 1.1
10
+ #
11
+ # Copyright (c) 2008 Microsoft Corporation. All Rights Reserved.
12
+ #######################################################################
13
+
14
+ require 'cgi'
15
+ require 'uri'
16
+ require 'base64'
17
+ require 'openssl'
18
+ require 'net/https'
19
+ require 'rexml/document'
20
+
21
+ module OmniAuth; module Strategies; class WindowsLive; class WindowsLiveLogin
22
+
23
+ #####################################################################
24
+ # Stub implementation for logging errors. If you want to enable
25
+ # debugging output using the default mechanism, specify true.
26
+ # By default, debug information will be printed to the standard
27
+ # error output and should be visible in the web server logs.
28
+ #####################################################################
29
+ def setDebug(flag)
30
+ @debug = flag
31
+ end
32
+
33
+ #####################################################################
34
+ # Stub implementation for logging errors. By default, this function
35
+ # does nothing if the debug flag has not been set with setDebug.
36
+ # Otherwise, it tries to log the error message.
37
+ #####################################################################
38
+ def debug(error)
39
+ return unless @debug
40
+ return if error.nil? or error.empty?
41
+ warn("Windows Live ID Authentication SDK #{error}")
42
+ nil
43
+ end
44
+
45
+ #####################################################################
46
+ # Stub implementation for handling a fatal error.
47
+ #####################################################################
48
+ def fatal(error)
49
+ debug(error)
50
+ raise(error)
51
+ end
52
+
53
+ #####################################################################
54
+ # Initialize the WindowsLiveLogin module with the application ID,
55
+ # secret key, and security algorithm.
56
+ #
57
+ # We recommend that you employ strong measures to protect the
58
+ # secret key. The secret key should never be exposed to the Web
59
+ # or other users.
60
+ #
61
+ # Be aware that if you do not supply these settings at
62
+ # initialization time, you may need to set the corresponding
63
+ # properties manually.
64
+ #
65
+ # For Delegated Authentication, you may optionally specify the
66
+ # privacy policy URL and return URL. If you do not specify these
67
+ # values here, the default values that you specified when you
68
+ # registered your application will be used.
69
+ #
70
+ # The 'force_delauth_nonprovisioned' flag also indicates whether
71
+ # your application is registered for Delegated Authentication
72
+ # (that is, whether it uses an application ID and secret key). We
73
+ # recommend that your Delegated Authentication application always
74
+ # be registered for enhanced security and functionality.
75
+ #####################################################################
76
+ def initialize(appid=nil, secret=nil, securityalgorithm=nil,
77
+ force_delauth_nonprovisioned=nil,
78
+ policyurl=nil, returnurl=nil)
79
+ self.force_delauth_nonprovisioned = force_delauth_nonprovisioned
80
+ self.appid = appid if appid
81
+ self.secret = secret if secret
82
+ self.securityalgorithm = securityalgorithm if securityalgorithm
83
+ self.policyurl = policyurl if policyurl
84
+ self.returnurl = returnurl if returnurl
85
+ end
86
+
87
+ #####################################################################
88
+ # Initialize the WindowsLiveLogin module from a settings file.
89
+ #
90
+ # 'settingsFile' specifies the location of the XML settings file
91
+ # that contains the application ID, secret key, and security
92
+ # algorithm. The file is of the following format:
93
+ #
94
+ # <windowslivelogin>
95
+ # <appid>APPID</appid>
96
+ # <secret>SECRET</secret>
97
+ # <securityalgorithm>wsignin1.0</securityalgorithm>
98
+ # </windowslivelogin>
99
+ #
100
+ # In a Delegated Authentication scenario, you may also specify
101
+ # 'returnurl' and 'policyurl' in the settings file, as shown in the
102
+ # Delegated Authentication samples.
103
+ #
104
+ # We recommend that you store the WindowsLiveLogin settings file
105
+ # in an area on your server that cannot be accessed through the
106
+ # Internet. This file contains important confidential information.
107
+ #####################################################################
108
+ def self.initFromXml(settingsFile)
109
+ o = self.new
110
+ settings = o.parseSettings(settingsFile)
111
+
112
+ o.setDebug(settings['debug'] == 'true')
113
+ o.force_delauth_nonprovisioned =
114
+ (settings['force_delauth_nonprovisioned'] == 'true')
115
+
116
+ o.appid = settings['appid']
117
+ o.secret = settings['secret']
118
+ o.oldsecret = settings['oldsecret']
119
+ o.oldsecretexpiry = settings['oldsecretexpiry']
120
+ o.securityalgorithm = settings['securityalgorithm']
121
+ o.policyurl = settings['policyurl']
122
+ o.returnurl = settings['returnurl']
123
+ o.baseurl = settings['baseurl']
124
+ o.secureurl = settings['secureurl']
125
+ o.consenturl = settings['consenturl']
126
+ o
127
+ end
128
+
129
+ #####################################################################
130
+ # Sets the application ID. Use this method if you did not specify
131
+ # an application ID at initialization.
132
+ #####################################################################
133
+ def appid=(appid)
134
+ if (appid.nil? or appid.empty?)
135
+ return if force_delauth_nonprovisioned
136
+ fatal("Error: appid: Null application ID.")
137
+ end
138
+ if (not appid =~ /^\w+$/)
139
+ fatal("Error: appid: Application ID must be alpha-numeric: " + appid)
140
+ end
141
+ @appid = appid
142
+ end
143
+
144
+ #####################################################################
145
+ # Returns the application ID.
146
+ #####################################################################
147
+ def appid
148
+ if (@appid.nil? or @appid.empty?)
149
+ fatal("Error: appid: App ID was not set. Aborting.")
150
+ end
151
+ @appid
152
+ end
153
+
154
+ #####################################################################
155
+ # Sets your secret key. Use this method if you did not specify
156
+ # a secret key at initialization.
157
+ #####################################################################
158
+ def secret=(secret)
159
+ if (secret.nil? or secret.empty?)
160
+ return if force_delauth_nonprovisioned
161
+ fatal("Error: secret=: Secret must be non-null.")
162
+ end
163
+ if (secret.size < 16)
164
+ fatal("Error: secret=: Secret must be at least 16 characters.")
165
+ end
166
+ @signkey = derive(secret, "SIGNATURE")
167
+ @cryptkey = derive(secret, "ENCRYPTION")
168
+ end
169
+
170
+ #####################################################################
171
+ # Sets your old secret key.
172
+ #
173
+ # Use this property to set your old secret key if you are in the
174
+ # process of transitioning to a new secret key. You may need this
175
+ # property because the Windows Live ID servers can take up to
176
+ # 24 hours to propagate a new secret key after you have updated
177
+ # your application settings.
178
+ #
179
+ # If an old secret key is specified here and has not expired
180
+ # (as determined by the oldsecretexpiry setting), it will be used
181
+ # as a fallback if token decryption fails with the new secret
182
+ # key.
183
+ #####################################################################
184
+ def oldsecret=(secret)
185
+ return if (secret.nil? or secret.empty?)
186
+ if (secret.size < 16)
187
+ fatal("Error: oldsecret=: Secret must be at least 16 characters.")
188
+ end
189
+ @oldsignkey = derive(secret, "SIGNATURE")
190
+ @oldcryptkey = derive(secret, "ENCRYPTION")
191
+ end
192
+
193
+ #####################################################################
194
+ # Sets the expiry time for your old secret key.
195
+ #
196
+ # After this time has passed, the old secret key will no longer be
197
+ # used even if token decryption fails with the new secret key.
198
+ #
199
+ # The old secret expiry time is represented as the number of seconds
200
+ # elapsed since January 1, 1970.
201
+ #####################################################################
202
+ def oldsecretexpiry=(timestamp)
203
+ return if (timestamp.nil? or timestamp.empty?)
204
+ timestamp = timestamp.to_i
205
+ fatal("Error: oldsecretexpiry=: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
206
+ @oldsecretexpiry = Time.at timestamp
207
+ end
208
+
209
+ #####################################################################
210
+ # Gets the old secret key expiry time.
211
+ #####################################################################
212
+ attr_accessor :oldsecretexpiry
213
+
214
+ #####################################################################
215
+ # Sets or gets the version of the security algorithm being used.
216
+ #####################################################################
217
+ attr_accessor :securityalgorithm
218
+
219
+ def securityalgorithm
220
+ if(@securityalgorithm.nil? or @securityalgorithm.empty?)
221
+ "wsignin1.0"
222
+ else
223
+ @securityalgorithm
224
+ end
225
+ end
226
+
227
+ #####################################################################
228
+ # Sets a flag that indicates whether Delegated Authentication
229
+ # is non-provisioned (i.e. does not use an application ID or secret
230
+ # key).
231
+ #####################################################################
232
+ attr_accessor :force_delauth_nonprovisioned
233
+
234
+ #####################################################################
235
+ # Sets the privacy policy URL, to which the Windows Live ID consent
236
+ # service redirects users to view the privacy policy of your Web
237
+ # site for Delegated Authentication.
238
+ #####################################################################
239
+ def policyurl=(policyurl)
240
+ if ((policyurl.nil? or policyurl.empty?) and force_delauth_nonprovisioned)
241
+ fatal("Error: policyurl=: Invalid policy URL specified.")
242
+ end
243
+ @policyurl = policyurl
244
+ end
245
+
246
+ #####################################################################
247
+ # Gets the privacy policy URL for your site.
248
+ #####################################################################
249
+ def policyurl
250
+ if (@policyurl.nil? or @policyurl.empty?)
251
+ debug("Warning: In the initial release of Del Auth, a Policy URL must be configured in the SDK for both provisioned and non-provisioned scenarios.")
252
+ raise("Error: policyurl: Policy URL must be set in a Del Auth non-provisioned scenario. Aborting.") if force_delauth_nonprovisioned
253
+ end
254
+ @policyurl
255
+ end
256
+
257
+ #####################################################################
258
+ # Sets the return URL--the URL on your site to which the consent
259
+ # service redirects users (along with the action, consent token,
260
+ # and application context) after they have successfully provided
261
+ # consent information for Delegated Authentication. This value will
262
+ # override the return URL specified during registration.
263
+ #####################################################################
264
+ def returnurl=(returnurl)
265
+ if ((returnurl.nil? or returnurl.empty?) and force_delauth_nonprovisioned)
266
+ fatal("Error: returnurl=: Invalid return URL specified.")
267
+ end
268
+ @returnurl = returnurl
269
+ end
270
+
271
+
272
+ #####################################################################
273
+ # Returns the return URL of your site.
274
+ #####################################################################
275
+ def returnurl
276
+ if ((@returnurl.nil? or @returnurl.empty?) and force_delauth_nonprovisioned)
277
+ fatal("Error: returnurl: Return URL must be set in a Del Auth non-provisioned scenario. Aborting.")
278
+ end
279
+ @returnurl
280
+ end
281
+
282
+ #####################################################################
283
+ # Sets or gets the base URL to use for the Windows Live Login server. You
284
+ # should not have to change this property. Furthermore, we recommend
285
+ # that you use the Sign In control instead of the URL methods
286
+ # provided here.
287
+ #####################################################################
288
+ attr_accessor :baseurl
289
+
290
+ def baseurl
291
+ if(@baseurl.nil? or @baseurl.empty?)
292
+ "http://login.live.com/"
293
+ else
294
+ @baseurl
295
+ end
296
+ end
297
+
298
+ #####################################################################
299
+ # Sets or gets the secure (HTTPS) URL to use for the Windows Live Login
300
+ # server. You should not have to change this property.
301
+ #####################################################################
302
+ attr_accessor :secureurl
303
+
304
+ def secureurl
305
+ if(@secureurl.nil? or @secureurl.empty?)
306
+ "https://login.live.com/"
307
+ else
308
+ @secureurl
309
+ end
310
+ end
311
+
312
+ #####################################################################
313
+ # Sets or gets the Consent Base URL to use for the Windows Live Consent
314
+ # server. You should not have to use or change this property directly.
315
+ #####################################################################
316
+ attr_accessor :consenturl
317
+
318
+ def consenturl
319
+ if(@consenturl.nil? or @consenturl.empty?)
320
+ "https://consent.live.com/"
321
+ else
322
+ @consenturl
323
+ end
324
+ end
325
+ end
326
+
327
+ #######################################################################
328
+ # Implementation of the basic methods needed for Web Authentication.
329
+ #######################################################################
330
+ class WindowsLiveLogin
331
+ #####################################################################
332
+ # Returns the sign-in URL to use for the Windows Live Login server.
333
+ # We recommend that you use the Sign In control instead.
334
+ #
335
+ # If you specify it, 'context' will be returned as-is in the sign-in
336
+ # response for site-specific use.
337
+ #####################################################################
338
+ def getLoginUrl(context=nil, market=nil)
339
+ url = baseurl + "wlogin.srf?appid=#{appid}"
340
+ url += "&alg=#{securityalgorithm}"
341
+ url += "&appctx=#{CGI.escape(context)}" if context
342
+ url += "&mkt=#{CGI.escape(market)}" if market
343
+ url
344
+ end
345
+
346
+ #####################################################################
347
+ # Returns the sign-out URL to use for the Windows Live Login server.
348
+ # We recommend that you use the Sign In control instead.
349
+ #####################################################################
350
+ def getLogoutUrl(market=nil)
351
+ url = baseurl + "logout.srf?appid=#{appid}"
352
+ url += "&mkt=#{CGI.escape(market)}" if market
353
+ url
354
+ end
355
+
356
+ #####################################################################
357
+ # Holds the user information after a successful sign-in.
358
+ #
359
+ # 'timestamp' is the time as obtained from the SSO token.
360
+ # 'id' is the pairwise unique ID for the user.
361
+ # 'context' is the application context that was originally passed to
362
+ # the sign-in request, if any.
363
+ # 'token' is the encrypted Web Authentication token that contains the
364
+ # UID. This can be cached in a cookie and the UID can be retrieved by
365
+ # calling the processToken method.
366
+ # 'usePersistentCookie?' indicates whether the application is
367
+ # expected to store the user token in a session or persistent
368
+ # cookie.
369
+ #####################################################################
370
+ class User
371
+ attr_reader :timestamp, :id, :context, :token
372
+
373
+ def usePersistentCookie?
374
+ @usePersistentCookie
375
+ end
376
+
377
+
378
+ #####################################################################
379
+ # Initialize the User with time stamp, userid, flags, context and token.
380
+ #####################################################################
381
+ def initialize(timestamp, id, flags, context, token)
382
+ self.timestamp = timestamp
383
+ self.id = id
384
+ self.flags = flags
385
+ self.context = context
386
+ self.token = token
387
+ end
388
+
389
+ private
390
+ attr_writer :timestamp, :id, :flags, :context, :token
391
+
392
+ #####################################################################
393
+ # Sets or gets the Unix timestamp as obtained from the SSO token.
394
+ #####################################################################
395
+ def timestamp=(timestamp)
396
+ raise("Error: User: Null timestamp in token.") unless timestamp
397
+ timestamp = timestamp.to_i
398
+ raise("Error: User: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
399
+ @timestamp = Time.at timestamp
400
+ end
401
+
402
+ #####################################################################
403
+ # Sets or gets the pairwise unique ID for the user.
404
+ #####################################################################
405
+ def id=(id)
406
+ raise("Error: User: Null id in token.") unless id
407
+ raise("Error: User: Invalid id: #{id}") unless (id =~ /^\w+$/)
408
+ @id = id
409
+ end
410
+
411
+ #####################################################################
412
+ # Sets or gets the usePersistentCookie flag for the user.
413
+ #####################################################################
414
+ def flags=(flags)
415
+ @usePersistentCookie = false
416
+ if flags
417
+ @usePersistentCookie = ((flags.to_i % 2) == 1)
418
+ end
419
+ end
420
+ end
421
+
422
+ #####################################################################
423
+ # Processes the sign-in response from the Windows Live sign-in server.
424
+ #
425
+ # 'query' contains the preprocessed POST table, such as that
426
+ # returned by CGI.params or Rails. (The unprocessed POST string
427
+ # could also be used here but we do not recommend it).
428
+ #
429
+ # This method returns a User object on successful sign-in; otherwise
430
+ # it returns nil.
431
+ #####################################################################
432
+ def processLogin(query)
433
+ query = parse query
434
+ unless query
435
+ debug("Error: processLogin: Failed to parse query.")
436
+ return
437
+ end
438
+ action = query['action']
439
+ unless action == 'login'
440
+ debug("Warning: processLogin: query action ignored: #{action}.")
441
+ return
442
+ end
443
+ token = query['stoken']
444
+ context = CGI.unescape(query['appctx']) if query['appctx']
445
+ processToken(token, context)
446
+ end
447
+
448
+ #####################################################################
449
+ # Decodes and validates a Web Authentication token. Returns a User
450
+ # object on success. If a context is passed in, it will be returned
451
+ # as the context field in the User object.
452
+ #####################################################################
453
+ def processToken(token, context=nil)
454
+ if token.nil? or token.empty?
455
+ debug("Error: processToken: Null/empty token.")
456
+ return
457
+ end
458
+ stoken = decodeAndValidateToken token
459
+ stoken = parse stoken
460
+ unless stoken
461
+ debug("Error: processToken: Failed to decode/validate token: #{token}")
462
+ return
463
+ end
464
+ sappid = stoken['appid']
465
+ unless sappid == appid
466
+ debug("Error: processToken: Application ID in token did not match ours: #{sappid}, #{appid}")
467
+ return
468
+ end
469
+ begin
470
+ user = User.new(stoken['ts'], stoken['uid'], stoken['flags'],
471
+ context, token)
472
+ return user
473
+ rescue Exception => e
474
+ debug("Error: processToken: Contents of token considered invalid: #{e}")
475
+ return
476
+ end
477
+ end
478
+
479
+ #####################################################################
480
+ # Returns an appropriate content type and body response that the
481
+ # application handler can return to signify a successful sign-out
482
+ # from the application.
483
+ #
484
+ # When a user signs out of Windows Live or a Windows Live
485
+ # application, a best-effort attempt is made at signing the user out
486
+ # from all other Windows Live applications the user might be signed
487
+ # in to. This is done by calling the handler page for each
488
+ # application with 'action' set to 'clearcookie' in the query
489
+ # string. The application handler is then responsible for clearing
490
+ # any cookies or data associated with the sign-in. After successfully
491
+ # signing the user out, the handler should return a GIF (any GIF)
492
+ # image as response to the 'action=clearcookie' query.
493
+ #####################################################################
494
+ def getClearCookieResponse()
495
+ type = "image/gif"
496
+ content = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7"
497
+ content = Base64.decode64(content)
498
+ return type, content
499
+ end
500
+ end
501
+
502
+ #######################################################################
503
+ # Implementation of the basic methods needed for Delegated
504
+ # Authentication.
505
+ #######################################################################
506
+ class WindowsLiveLogin
507
+ #####################################################################
508
+ # Returns the consent URL to use for Delegated Authentication for
509
+ # the given comma-delimited list of offers.
510
+ #
511
+ # If you specify it, 'context' will be returned as-is in the consent
512
+ # response for site-specific use.
513
+ #
514
+ # The registered/configured return URL can also be overridden by
515
+ # specifying 'ru' here.
516
+ #
517
+ # You can change the language in which the consent page is displayed
518
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
519
+ # 'market' parameter.
520
+ #####################################################################
521
+ def getConsentUrl(offers, context=nil, ru=nil, market=nil)
522
+ if (offers.nil? or offers.empty?)
523
+ fatal("Error: getConsentUrl: Invalid offers list.")
524
+ end
525
+ url = consenturl + "Delegation.aspx?ps=#{CGI.escape(offers)}"
526
+ url += "&appctx=#{CGI.escape(context)}" if context
527
+ ru = returnurl if (ru.nil? or ru.empty?)
528
+ url += "&ru=#{CGI.escape(ru)}" if ru
529
+ pu = policyurl
530
+ url += "&pl=#{CGI.escape(pu)}" if pu
531
+ url += "&mkt=#{CGI.escape(market)}" if market
532
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
533
+ url
534
+ end
535
+
536
+ #####################################################################
537
+ # Returns the URL to use to download a new consent token, given the
538
+ # offers and refresh token.
539
+ # The registered/configured return URL can also be overridden by
540
+ # specifying 'ru' here.
541
+ #####################################################################
542
+ def getRefreshConsentTokenUrl(offers, refreshtoken, ru)
543
+ if (offers.nil? or offers.empty?)
544
+ fatal("Error: getRefreshConsentTokenUrl: Invalid offers list.")
545
+ end
546
+ if (refreshtoken.nil? or refreshtoken.empty?)
547
+ fatal("Error: getRefreshConsentTokenUrl: Invalid refresh token.")
548
+ end
549
+ url = consenturl + "RefreshToken.aspx?ps=#{CGI.escape(offers)}"
550
+ url += "&reft=#{refreshtoken}"
551
+ ru = returnurl if (ru.nil? or ru.empty?)
552
+ url += "&ru=#{CGI.escape(ru)}" if ru
553
+ url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
554
+ url
555
+ end
556
+
557
+ #####################################################################
558
+ # Returns the URL for the consent-management user interface.
559
+ # You can change the language in which the consent page is displayed
560
+ # by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
561
+ # 'market' parameter.
562
+ #####################################################################
563
+ def getManageConsentUrl(market=nil)
564
+ url = consenturl + "ManageConsent.aspx"
565
+ url += "?mkt=#{CGI.escape(market)}" if market
566
+ url
567
+ end
568
+
569
+ class ConsentToken
570
+ attr_reader :delegationtoken, :refreshtoken, :sessionkey, :expiry
571
+ attr_reader :offers, :offers_string, :locationid, :context
572
+ attr_reader :decodedtoken, :token
573
+
574
+ #####################################################################
575
+ # Indicates whether the delegation token is set and has not expired.
576
+ #####################################################################
577
+ def isValid?
578
+ return false unless delegationtoken
579
+ return ((Time.now.to_i-300) < expiry.to_i)
580
+ end
581
+
582
+ #####################################################################
583
+ # Refreshes the current token and replace it. If operation succeeds
584
+ # true is returned to signify success.
585
+ #####################################################################
586
+ def refresh
587
+ ct = @wll.refreshConsentToken(self)
588
+ return false unless ct
589
+ copy(ct)
590
+ true
591
+ end
592
+
593
+ #####################################################################
594
+ # Initialize the ConsentToken module with the WindowsLiveLogin,
595
+ # delegation token, refresh token, session key, expiry, offers,
596
+ # location ID, context, decoded token, and raw token.
597
+ #####################################################################
598
+ def initialize(wll, delegationtoken, refreshtoken, sessionkey, expiry,
599
+ offers, locationid, context, decodedtoken, token)
600
+ @wll = wll
601
+ self.delegationtoken = delegationtoken
602
+ self.refreshtoken = refreshtoken
603
+ self.sessionkey = sessionkey
604
+ self.expiry = expiry
605
+ self.offers = offers
606
+ self.locationid = locationid
607
+ self.context = context
608
+ self.decodedtoken = decodedtoken
609
+ self.token = token
610
+ end
611
+
612
+ private
613
+ attr_writer :delegationtoken, :refreshtoken, :sessionkey, :expiry
614
+ attr_writer :offers, :offers_string, :locationid, :context
615
+ attr_writer :decodedtoken, :token, :locationid
616
+
617
+ #####################################################################
618
+ # Sets the delegation token.
619
+ #####################################################################
620
+ def delegationtoken=(delegationtoken)
621
+ if (delegationtoken.nil? or delegationtoken.empty?)
622
+ raise("Error: ConsentToken: Null delegation token.")
623
+ end
624
+ @delegationtoken = delegationtoken
625
+ end
626
+
627
+ #####################################################################
628
+ # Sets the session key.
629
+ #####################################################################
630
+ def sessionkey=(sessionkey)
631
+ if (sessionkey.nil? or sessionkey.empty?)
632
+ raise("Error: ConsentToken: Null session key.")
633
+ end
634
+ @sessionkey = @wll.u64(sessionkey)
635
+ end
636
+
637
+ #####################################################################
638
+ # Sets the expiry time of the delegation token.
639
+ #####################################################################
640
+ def expiry=(expiry)
641
+ if (expiry.nil? or expiry.empty?)
642
+ raise("Error: ConsentToken: Null expiry time.")
643
+ end
644
+ expiry = expiry.to_i
645
+ raise("Error: ConsentToken: Invalid expiry: #{expiry}") if (expiry <= 0)
646
+ @expiry = Time.at expiry
647
+ end
648
+
649
+ #####################################################################
650
+ # Sets the offers/actions for which the user granted consent.
651
+ #####################################################################
652
+ def offers=(offers)
653
+ if (offers.nil? or offers.empty?)
654
+ raise("Error: ConsentToken: Null offers.")
655
+ end
656
+
657
+ @offers_string = ""
658
+ @offers = []
659
+
660
+ offers = CGI.unescape(offers)
661
+ offers = offers.split(";")
662
+ offers.each{|offer|
663
+ offer = offer.split(":")[0]
664
+ @offers_string += "," unless @offers_string.empty?
665
+ @offers_string += offer
666
+ @offers.push(offer)
667
+ }
668
+ end
669
+
670
+ #####################################################################
671
+ # Sets the LocationID.
672
+ #####################################################################
673
+ def locationid=(locationid)
674
+ if (locationid.nil? or locationid.empty?)
675
+ raise("Error: ConsentToken: Null Location ID.")
676
+ end
677
+ @locationid = locationid
678
+ end
679
+
680
+ #####################################################################
681
+ # Makes a copy of the ConsentToken object.
682
+ #####################################################################
683
+ def copy(consenttoken)
684
+ @delegationtoken = consenttoken.delegationtoken
685
+ @refreshtoken = consenttoken.refreshtoken
686
+ @sessionkey = consenttoken.sessionkey
687
+ @expiry = consenttoken.expiry
688
+ @offers = consenttoken.offers
689
+ @locationid = consenttoken.locationid
690
+ @offers_string = consenttoken.offers_string
691
+ @decodedtoken = consenttoken.decodedtoken
692
+ @token = consenttoken.token
693
+ end
694
+ end
695
+
696
+ #####################################################################
697
+ # Processes the POST response from the Delegated Authentication
698
+ # service after a user has granted consent. The processConsent
699
+ # function extracts the consent token string and returns the result
700
+ # of invoking the processConsentToken method.
701
+ #####################################################################
702
+ def processConsent(query)
703
+ query = parse query
704
+ unless query
705
+ debug("Error: processConsent: Failed to parse query.")
706
+ return
707
+ end
708
+ action = query['action']
709
+ unless action == 'delauth'
710
+ debug("Warning: processConsent: query action ignored: #{action}.")
711
+ return
712
+ end
713
+ responsecode = query['ResponseCode']
714
+ unless responsecode == 'RequestApproved'
715
+ debug("Error: processConsent: Consent was not successfully granted: #{responsecode}")
716
+ return
717
+ end
718
+ token = query['ConsentToken']
719
+ context = CGI.unescape(query['appctx']) if query['appctx']
720
+ processConsentToken(token, context)
721
+ end
722
+
723
+ #####################################################################
724
+ # Processes the consent token string that is returned in the POST
725
+ # response by the Delegated Authentication service after a
726
+ # user has granted consent.
727
+ #####################################################################
728
+ def processConsentToken(token, context=nil)
729
+ if token.nil? or token.empty?
730
+ debug("Error: processConsentToken: Null token.")
731
+ return
732
+ end
733
+ decodedtoken = token
734
+ parsedtoken = parse(CGI.unescape(decodedtoken))
735
+ unless parsedtoken
736
+ debug("Error: processConsentToken: Failed to parse token: #{token}")
737
+ return
738
+ end
739
+ eact = parsedtoken['eact']
740
+ if eact
741
+ decodedtoken = decodeAndValidateToken eact
742
+ unless decodedtoken
743
+ debug("Error: processConsentToken: Failed to decode/validate token: #{token}")
744
+ return
745
+ end
746
+ parsedtoken = parse(decodedtoken)
747
+ decodedtoken = CGI.escape(decodedtoken)
748
+ end
749
+ begin
750
+ consenttoken = ConsentToken.new(self,
751
+ parsedtoken['delt'],
752
+ parsedtoken['reft'],
753
+ parsedtoken['skey'],
754
+ parsedtoken['exp'],
755
+ parsedtoken['offer'],
756
+ parsedtoken['lid'],
757
+ context, decodedtoken, token)
758
+ return consenttoken
759
+ rescue Exception => e
760
+ debug("Error: processConsentToken: Contents of token considered invalid: #{e}")
761
+ return
762
+ end
763
+ end
764
+
765
+ #####################################################################
766
+ # Attempts to obtain a new, refreshed token and return it. The
767
+ # original token is not modified.
768
+ #####################################################################
769
+ def refreshConsentToken(consenttoken, ru=nil)
770
+ if consenttoken.nil?
771
+ debug("Error: refreshConsentToken: Null consent token.")
772
+ return
773
+ end
774
+ refreshConsentToken2(consenttoken.offers_string, consenttoken.refreshtoken, ru)
775
+ end
776
+
777
+ #####################################################################
778
+ # Helper function to obtain a new, refreshed token and return it.
779
+ # The original token is not modified.
780
+ #####################################################################
781
+ def refreshConsentToken2(offers_string, refreshtoken, ru=nil)
782
+ url = nil
783
+ begin
784
+ url = getRefreshConsentTokenUrl(offers_string, refreshtoken, ru)
785
+ ret = fetch url
786
+ ret.value # raises exception if fetch failed
787
+ body = ret.body
788
+ body.scan(/\{"ConsentToken":"(.*)"\}/){|match|
789
+ return processConsentToken("#{match}")
790
+ }
791
+ debug("Error: refreshConsentToken2: Failed to extract token: #{body}")
792
+ rescue Exception => e
793
+ debug("Error: Failed to refresh consent token: #{e}")
794
+ end
795
+ return
796
+ end
797
+ end
798
+
799
+ #######################################################################
800
+ # Common methods.
801
+ #######################################################################
802
+ class WindowsLiveLogin
803
+
804
+ #####################################################################
805
+ # Decodes and validates the token.
806
+ #####################################################################
807
+ def decodeAndValidateToken(token, cryptkey=@cryptkey, signkey=@signkey,
808
+ internal_allow_recursion=true)
809
+ haveoldsecret = false
810
+ if (oldsecretexpiry and (Time.now.to_i < oldsecretexpiry.to_i))
811
+ haveoldsecret = true if (@oldcryptkey and @oldsignkey)
812
+ end
813
+ haveoldsecret = (haveoldsecret and internal_allow_recursion)
814
+
815
+ stoken = decodeToken(token, cryptkey)
816
+ stoken = validateToken(stoken, signkey) if stoken
817
+ if (stoken.nil? and haveoldsecret)
818
+ debug("Warning: Failed to validate token with current secret, attempting old secret.")
819
+ stoken = decodeAndValidateToken(token, @oldcryptkey, @oldsignkey, false)
820
+ end
821
+ stoken
822
+ end
823
+
824
+ #####################################################################
825
+ # Decodes the given token string; returns undef on failure.
826
+ #
827
+ # First, the string is URL-unescaped and base64 decoded.
828
+ # Second, the IV is extracted from the first 16 bytes of the string.
829
+ # Finally, the string is decrypted using the encryption key.
830
+ #####################################################################
831
+ def decodeToken(token, cryptkey=@cryptkey)
832
+ if (cryptkey.nil? or cryptkey.empty?)
833
+ fatal("Error: decodeToken: Secret key was not set. Aborting.")
834
+ end
835
+ token = u64(token)
836
+ if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
837
+ debug("Error: decodeToken: Attempted to decode invalid token.")
838
+ return
839
+ end
840
+ iv = token[0..15]
841
+ crypted = token[16..-1]
842
+ begin
843
+ aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
844
+ aes128cbc.decrypt
845
+ aes128cbc.iv = iv
846
+ aes128cbc.key = cryptkey
847
+ decrypted = aes128cbc.update(crypted) + aes128cbc.final
848
+ rescue Exception => e
849
+ debug("Error: decodeToken: Decryption failed: #{token}, #{e}")
850
+ return
851
+ end
852
+ decrypted
853
+ end
854
+
855
+ #####################################################################
856
+ # Creates a signature for the given string by using the signature
857
+ # key.
858
+ #####################################################################
859
+ def signToken(token, signkey=@signkey)
860
+ if (signkey.nil? or signkey.empty?)
861
+ fatal("Error: signToken: Secret key was not set. Aborting.")
862
+ end
863
+ begin
864
+ digest = OpenSSL::Digest::SHA256.new
865
+ return OpenSSL::HMAC.digest(digest, signkey, token)
866
+ rescue Exception => e
867
+ debug("Error: signToken: Signing failed: #{token}, #{e}")
868
+ return
869
+ end
870
+ end
871
+
872
+ #####################################################################
873
+ # Extracts the signature from the token and validates it.
874
+ #####################################################################
875
+ def validateToken(token, signkey=@signkey)
876
+ if (token.nil? or token.empty?)
877
+ debug("Error: validateToken: Null token.")
878
+ return
879
+ end
880
+ body, sig = token.split("&sig=")
881
+ if (body.nil? or sig.nil?)
882
+ debug("Error: validateToken: Invalid token: #{token}")
883
+ return
884
+ end
885
+ sig = u64(sig)
886
+ return token if (sig == signToken(body, signkey))
887
+ debug("Error: validateToken: Signature did not match.")
888
+ return
889
+ end
890
+ end
891
+
892
+ #######################################################################
893
+ # Implementation of the methods needed to perform Windows Live
894
+ # application verification as well as trusted sign-in.
895
+ #######################################################################
896
+ class WindowsLiveLogin
897
+ #####################################################################
898
+ # Generates an application verifier token. An IP address can
899
+ # optionally be included in the token.
900
+ #####################################################################
901
+ def getAppVerifier(ip=nil)
902
+ token = "appid=#{appid}&ts=#{timestamp}"
903
+ token += "&ip=#{ip}" if ip
904
+ token += "&sig=#{e64(signToken(token))}"
905
+ CGI.escape token
906
+ end
907
+
908
+ #####################################################################
909
+ # Returns the URL that is required to retrieve the application
910
+ # security token.
911
+ #
912
+ # By default, the application security token is generated for
913
+ # the Windows Live site; a specific Site ID can optionally be
914
+ # specified in 'siteid'. The IP address can also optionally be
915
+ # included in 'ip'.
916
+ #
917
+ # If 'js' is nil, a JavaScript Output Notation (JSON) response is
918
+ # returned in the following format:
919
+ #
920
+ # {"token":"<value>"}
921
+ #
922
+ # Otherwise, a JavaScript response is returned. It is assumed that
923
+ # WLIDResultCallback is a custom function implemented to handle the
924
+ # token value:
925
+ #
926
+ # WLIDResultCallback("<tokenvalue>");
927
+ #####################################################################
928
+ def getAppLoginUrl(siteid=nil, ip=nil, js=nil)
929
+ url = secureurl + "wapplogin.srf?app=#{getAppVerifier(ip)}"
930
+ url += "&alg=#{securityalgorithm}"
931
+ url += "&id=#{siteid}" if siteid
932
+ url += "&js=1" if js
933
+ url
934
+ end
935
+
936
+ #####################################################################
937
+ # Retrieves the application security token for application
938
+ # verification from the application sign-in URL.
939
+ #
940
+ # By default, the application security token will be generated for
941
+ # the Windows Live site; a specific Site ID can optionally be
942
+ # specified in 'siteid'. The IP address can also optionally be
943
+ # included in 'ip'.
944
+ #
945
+ # Implementation note: The application security token is downloaded
946
+ # from the application sign-in URL in JSON format:
947
+ #
948
+ # {"token":"<value>"}
949
+ #
950
+ # Therefore we must extract <value> from the string and return it as
951
+ # seen here.
952
+ #####################################################################
953
+ def getAppSecurityToken(siteid=nil, ip=nil)
954
+ url = getAppLoginUrl(siteid, ip)
955
+ begin
956
+ ret = fetch url
957
+ ret.value # raises exception if fetch failed
958
+ body = ret.body
959
+ body.scan(/\{"token":"(.*)"\}/){|match|
960
+ return match
961
+ }
962
+ debug("Error: getAppSecurityToken: Failed to extract token: #{body}")
963
+ rescue Exception => e
964
+ debug("Error: getAppSecurityToken: Failed to get token: #{e}")
965
+ end
966
+ return
967
+ end
968
+
969
+ #####################################################################
970
+ # Returns a string that can be passed to the getTrustedParams
971
+ # function as the 'retcode' parameter. If this is specified as the
972
+ # 'retcode', the application will be used as return URL after it
973
+ # finishes trusted sign-in.
974
+ #####################################################################
975
+ def getAppRetCode
976
+ "appid=#{appid}"
977
+ end
978
+
979
+ #####################################################################
980
+ # Returns a table of key-value pairs that must be posted to the
981
+ # sign-in URL for trusted sign-in. Use HTTP POST to do this. Be aware
982
+ # that the values in the table are neither URL nor HTML escaped and
983
+ # may have to be escaped if you are inserting them in code such as
984
+ # an HTML form.
985
+ #
986
+ # The user to be trusted on the local site is passed in as string
987
+ # 'user'.
988
+ #
989
+ # Optionally, 'retcode' specifies the resource to which successful
990
+ # sign-in is redirected, such as Windows Live Mail, and is typically
991
+ # a string in the format 'id=2000'. If you pass in the value from
992
+ # getAppRetCode instead, sign-in will be redirected to the
993
+ # application. Otherwise, an HTTP 200 response is returned.
994
+ #####################################################################
995
+ def getTrustedParams(user, retcode=nil)
996
+ token = getTrustedToken(user)
997
+ return unless token
998
+ token = %{<wst:RequestSecurityTokenResponse xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust"><wst:RequestedSecurityToken><wsse:BinarySecurityToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">#{token}</wsse:BinarySecurityToken></wst:RequestedSecurityToken><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"><wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"><wsa:Address>uri:WindowsLiveID</wsa:Address></wsa:EndpointReference></wsp:AppliesTo></wst:RequestSecurityTokenResponse>}
999
+ params = {}
1000
+ params['wa'] = securityalgorithm
1001
+ params['wresult'] = token
1002
+ params['wctx'] = retcode if retcode
1003
+ params
1004
+ end
1005
+
1006
+ #####################################################################
1007
+ # Returns the trusted sign-in token in the format that is needed by a
1008
+ # control doing trusted sign-in.
1009
+ #
1010
+ # The user to be trusted on the local site is passed in as string
1011
+ # 'user'.
1012
+ #####################################################################
1013
+ def getTrustedToken(user)
1014
+ if user.nil? or user.empty?
1015
+ debug('Error: getTrustedToken: Null user specified.')
1016
+ return
1017
+ end
1018
+ token = "appid=#{appid}&uid=#{CGI.escape(user)}&ts=#{timestamp}"
1019
+ token += "&sig=#{e64(signToken(token))}"
1020
+ CGI.escape token
1021
+ end
1022
+
1023
+ #####################################################################
1024
+ # Returns the trusted sign-in URL to use for the Windows Live Login
1025
+ # server.
1026
+ #####################################################################
1027
+ def getTrustedLoginUrl
1028
+ secureurl + "wlogin.srf"
1029
+ end
1030
+
1031
+ #####################################################################
1032
+ # Returns the trusted sign-out URL to use for the Windows Live Login
1033
+ # server.
1034
+ #####################################################################
1035
+ def getTrustedLogoutUrl
1036
+ secureurl + "logout.srf?appid=#{appid}"
1037
+ end
1038
+ end
1039
+
1040
+ #######################################################################
1041
+ # Helper methods.
1042
+ #######################################################################
1043
+ class WindowsLiveLogin
1044
+
1045
+ #######################################################################
1046
+ # Function to parse the settings file.
1047
+ #######################################################################
1048
+ def parseSettings(settingsFile)
1049
+ settings = {}
1050
+ begin
1051
+ file = File.new(settingsFile)
1052
+ doc = REXML::Document.new file
1053
+ root = doc.root
1054
+ root.each_element{|e|
1055
+ settings[e.name] = e.text
1056
+ }
1057
+ rescue Exception => e
1058
+ fatal("Error: parseSettings: Error while reading #{settingsFile}: #{e}")
1059
+ end
1060
+ return settings
1061
+ end
1062
+
1063
+ #####################################################################
1064
+ # Derives the key, given the secret key and prefix as described in the
1065
+ # Web Authentication SDK documentation.
1066
+ #####################################################################
1067
+ def derive(secret, prefix)
1068
+ begin
1069
+ fatal("Nil/empty secret.") if (secret.nil? or secret.empty?)
1070
+ key = prefix + secret
1071
+ key = OpenSSL::Digest::SHA256.digest(key)
1072
+ return key[0..15]
1073
+ rescue Exception => e
1074
+ debug("Error: derive: #{e}")
1075
+ return
1076
+ end
1077
+ end
1078
+
1079
+ #####################################################################
1080
+ # Parses query string and return a table
1081
+ # {String=>String}
1082
+ #
1083
+ # If a table is passed in from CGI.params, we convert it from
1084
+ # {String=>[]} to {String=>String}. I believe Rails uses symbols
1085
+ # instead of strings in general, so we convert from symbols to
1086
+ # strings here also.
1087
+ #####################################################################
1088
+ def parse(input)
1089
+ if (input.nil? or input.empty?)
1090
+ debug("Error: parse: Nil/empty input.")
1091
+ return
1092
+ end
1093
+
1094
+ pairs = {}
1095
+ if (input.class == String)
1096
+ input = input.split('&')
1097
+ input.each{|pair|
1098
+ k, v = pair.split('=')
1099
+ pairs[k] = v
1100
+ }
1101
+ else
1102
+ input.each{|k, v|
1103
+ v = v[0] if (v.class == Array)
1104
+ pairs[k.to_s] = v.to_s
1105
+ }
1106
+ end
1107
+ return pairs
1108
+ end
1109
+
1110
+ #####################################################################
1111
+ # Generates a time stamp suitable for the application verifier token.
1112
+ #####################################################################
1113
+ def timestamp
1114
+ Time.now.to_i.to_s
1115
+ end
1116
+
1117
+ #####################################################################
1118
+ # Base64-encodes and URL-escapes a string.
1119
+ #####################################################################
1120
+ def e64(s)
1121
+ return unless s
1122
+ CGI.escape Base64.encode64(s)
1123
+ end
1124
+
1125
+ #####################################################################
1126
+ # URL-unescapes and Base64-decodes a string.
1127
+ #####################################################################
1128
+ def u64(s)
1129
+ return unless s
1130
+ Base64.decode64 CGI.unescape(s)
1131
+ end
1132
+
1133
+ #####################################################################
1134
+ # Fetches the contents given a URL.
1135
+ #####################################################################
1136
+ def fetch(url)
1137
+ url = URI.parse url
1138
+ http = Net::HTTP.new(url.host, url.port)
1139
+ http.use_ssl = (url.scheme == "https")
1140
+ http.request_get url.request_uri
1141
+ end
1142
+ end end end end
1143
+