ruby-openid 1.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.
Files changed (114) hide show
  1. data/COPYING +21 -0
  2. data/INSTALL +34 -0
  3. data/README +67 -0
  4. data/TODO +9 -0
  5. data/examples/README +54 -0
  6. data/examples/cacert.pem +7815 -0
  7. data/examples/consumer.rb +285 -0
  8. data/examples/openid-store/associations/http-localhost_3A3000_2Fserver-EMQbAy3NnHVzA.s0u5KAcplKGzo +6 -0
  9. data/examples/openid-store/auth_key +1 -0
  10. data/examples/rails_active_record_store/README +59 -0
  11. data/examples/rails_active_record_store/XX_add_openidstore.rb +30 -0
  12. data/examples/rails_active_record_store/models/openid_association.rb +12 -0
  13. data/examples/rails_active_record_store/models/openid_nonce.rb +3 -0
  14. data/examples/rails_active_record_store/models/openid_setting.rb +2 -0
  15. data/examples/rails_active_record_store/openid_helper.rb +91 -0
  16. data/examples/rails_active_record_store/openidstore_test.rb +15 -0
  17. data/examples/rails_active_record_store/schema.mysql.sql +22 -0
  18. data/examples/rails_active_record_store/schema.postgresql.sql +21 -0
  19. data/examples/rails_active_record_store/schema.sqlite.sql +21 -0
  20. data/examples/rails_openid_login_generator/USAGE +23 -0
  21. data/examples/rails_openid_login_generator/openid_login_generator.rb +36 -0
  22. data/examples/rails_openid_login_generator/templates/README +116 -0
  23. data/examples/rails_openid_login_generator/templates/controller.rb +116 -0
  24. data/examples/rails_openid_login_generator/templates/controller_test.rb +0 -0
  25. data/examples/rails_openid_login_generator/templates/helper.rb +2 -0
  26. data/examples/rails_openid_login_generator/templates/openid_login_system.rb +87 -0
  27. data/examples/rails_openid_login_generator/templates/user.rb +14 -0
  28. data/examples/rails_openid_login_generator/templates/user_test.rb +0 -0
  29. data/examples/rails_openid_login_generator/templates/users.yml +0 -0
  30. data/examples/rails_openid_login_generator/templates/view_login.rhtml +15 -0
  31. data/examples/rails_openid_login_generator/templates/view_logout.rhtml +10 -0
  32. data/examples/rails_openid_login_generator/templates/view_welcome.rhtml +9 -0
  33. data/examples/rails_server/README +153 -0
  34. data/examples/rails_server/Rakefile +10 -0
  35. data/examples/rails_server/app/controllers/application.rb +4 -0
  36. data/examples/rails_server/app/controllers/login_controller.rb +35 -0
  37. data/examples/rails_server/app/controllers/server_controller.rb +185 -0
  38. data/examples/rails_server/app/helpers/application_helper.rb +3 -0
  39. data/examples/rails_server/app/helpers/login_helper.rb +2 -0
  40. data/examples/rails_server/app/helpers/server_helper.rb +9 -0
  41. data/examples/rails_server/app/views/layouts/server.rhtml +61 -0
  42. data/examples/rails_server/app/views/login/index.rhtml +32 -0
  43. data/examples/rails_server/app/views/server/decide.rhtml +11 -0
  44. data/examples/rails_server/config/boot.rb +19 -0
  45. data/examples/rails_server/config/database.yml +85 -0
  46. data/examples/rails_server/config/environment.rb +53 -0
  47. data/examples/rails_server/config/environments/development.rb +19 -0
  48. data/examples/rails_server/config/environments/production.rb +19 -0
  49. data/examples/rails_server/config/environments/test.rb +19 -0
  50. data/examples/rails_server/config/routes.rb +23 -0
  51. data/examples/rails_server/db/openid-store/associations/http-localhost_2F_7Cnormal-YU.tkND1J4fEZhnuAoT5Zc0yCA0 +6 -0
  52. data/examples/rails_server/doc/README_FOR_APP +2 -0
  53. data/examples/rails_server/log/development.log +6059 -0
  54. data/examples/rails_server/log/production.log +0 -0
  55. data/examples/rails_server/log/server.log +0 -0
  56. data/examples/rails_server/log/test.log +0 -0
  57. data/examples/rails_server/public/404.html +8 -0
  58. data/examples/rails_server/public/500.html +8 -0
  59. data/examples/rails_server/public/dispatch.cgi +12 -0
  60. data/examples/rails_server/public/dispatch.fcgi +26 -0
  61. data/examples/rails_server/public/dispatch.rb +12 -0
  62. data/examples/rails_server/public/favicon.ico +0 -0
  63. data/examples/rails_server/public/images/rails.png +0 -0
  64. data/examples/rails_server/public/javascripts/controls.js +750 -0
  65. data/examples/rails_server/public/javascripts/dragdrop.js +584 -0
  66. data/examples/rails_server/public/javascripts/effects.js +854 -0
  67. data/examples/rails_server/public/javascripts/prototype.js +1785 -0
  68. data/examples/rails_server/public/robots.txt +1 -0
  69. data/examples/rails_server/script/about +3 -0
  70. data/examples/rails_server/script/breakpointer +3 -0
  71. data/examples/rails_server/script/console +3 -0
  72. data/examples/rails_server/script/destroy +3 -0
  73. data/examples/rails_server/script/generate +3 -0
  74. data/examples/rails_server/script/performance/benchmarker +3 -0
  75. data/examples/rails_server/script/performance/profiler +3 -0
  76. data/examples/rails_server/script/plugin +3 -0
  77. data/examples/rails_server/script/process/reaper +3 -0
  78. data/examples/rails_server/script/process/spawner +3 -0
  79. data/examples/rails_server/script/process/spinner +3 -0
  80. data/examples/rails_server/script/runner +3 -0
  81. data/examples/rails_server/script/server +3 -0
  82. data/examples/rails_server/test/functional/login_controller_test.rb +18 -0
  83. data/examples/rails_server/test/functional/server_controller_test.rb +18 -0
  84. data/examples/rails_server/test/test_helper.rb +28 -0
  85. data/lib/hmac-md5.rb +11 -0
  86. data/lib/hmac-rmd160.rb +11 -0
  87. data/lib/hmac-sha1.rb +11 -0
  88. data/lib/hmac-sha2.rb +25 -0
  89. data/lib/hmac.rb +112 -0
  90. data/lib/openid/association.rb +109 -0
  91. data/lib/openid/consumer.rb +928 -0
  92. data/lib/openid/dh.rb +48 -0
  93. data/lib/openid/discovery.rb +89 -0
  94. data/lib/openid/fetchers.rb +119 -0
  95. data/lib/openid/filestore.rb +315 -0
  96. data/lib/openid/htmltokenizer.rb +355 -0
  97. data/lib/openid/parse.rb +23 -0
  98. data/lib/openid/server.rb +951 -0
  99. data/lib/openid/service.rb +135 -0
  100. data/lib/openid/stores.rb +178 -0
  101. data/lib/openid/trustroot.rb +100 -0
  102. data/lib/openid/util.rb +273 -0
  103. data/test/assoc.rb +38 -0
  104. data/test/consumer.rb +384 -0
  105. data/test/dh.rb +20 -0
  106. data/test/extensions.rb +30 -0
  107. data/test/linkparse.rb +305 -0
  108. data/test/runtests.rb +11 -0
  109. data/test/server2.rb +1053 -0
  110. data/test/storetestcase.rb +172 -0
  111. data/test/teststore.rb +23 -0
  112. data/test/trustroot.rb +113 -0
  113. data/test/util.rb +56 -0
  114. metadata +218 -0
@@ -0,0 +1 @@
1
+ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/about'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/breakpointer'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/console'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/destroy'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/generate'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/performance/benchmarker'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/performance/profiler'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/plugin'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/reaper'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/spawner'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/spinner'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/runner'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/server'
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'login_controller'
3
+
4
+ # Re-raise errors caught by the controller.
5
+ class LoginController; def rescue_action(e) raise e end; end
6
+
7
+ class LoginControllerTest < Test::Unit::TestCase
8
+ def setup
9
+ @controller = LoginController.new
10
+ @request = ActionController::TestRequest.new
11
+ @response = ActionController::TestResponse.new
12
+ end
13
+
14
+ # Replace this with your real tests.
15
+ def test_truth
16
+ assert true
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'server_controller'
3
+
4
+ # Re-raise errors caught by the controller.
5
+ class ServerController; def rescue_action(e) raise e end; end
6
+
7
+ class ServerControllerTest < Test::Unit::TestCase
8
+ def setup
9
+ @controller = ServerController.new
10
+ @request = ActionController::TestRequest.new
11
+ @response = ActionController::TestResponse.new
12
+ end
13
+
14
+ # Replace this with your real tests.
15
+ def test_truth
16
+ assert true
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
3
+ require 'test_help'
4
+
5
+ class Test::Unit::TestCase
6
+ # Transactional fixtures accelerate your tests by wrapping each test method
7
+ # in a transaction that's rolled back on completion. This ensures that the
8
+ # test database remains unchanged so your fixtures don't have to be reloaded
9
+ # between every test method. Fewer database queries means faster tests.
10
+ #
11
+ # Read Mike Clark's excellent walkthrough at
12
+ # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
13
+ #
14
+ # Every Active Record database supports transactions except MyISAM tables
15
+ # in MySQL. Turn off transactional fixtures in this case; however, if you
16
+ # don't care one way or the other, switching from MyISAM to InnoDB tables
17
+ # is recommended.
18
+ self.use_transactional_fixtures = true
19
+
20
+ # Instantiated fixtures are slow, but give you @david where otherwise you
21
+ # would need people(:david). If you don't want to migrate your existing
22
+ # test cases which use the @david style and don't mind the speed hit (each
23
+ # instantiated fixtures translates to a database query per test method),
24
+ # then set this back to true.
25
+ self.use_instantiated_fixtures = false
26
+
27
+ # Add more helper methods to be used by all tests here...
28
+ end
data/lib/hmac-md5.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'hmac'
2
+ require 'digest/md5'
3
+
4
+ module HMAC
5
+ class MD5 < Base
6
+ def initialize(key = nil)
7
+ super(Digest::MD5, 64, 16, key)
8
+ end
9
+ public_class_method :new, :digest, :hexdigest
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'hmac'
2
+ require 'digest/rmd160'
3
+
4
+ module HMAC
5
+ class RMD160 < Base
6
+ def initialize(key = nil)
7
+ super(Digest::RMD160, 64, 20, key)
8
+ end
9
+ public_class_method :new, :digest, :hexdigest
10
+ end
11
+ end
data/lib/hmac-sha1.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'hmac'
2
+ require 'digest/sha1'
3
+
4
+ module HMAC
5
+ class SHA1 < Base
6
+ def initialize(key = nil)
7
+ super(Digest::SHA1, 64, 20, key)
8
+ end
9
+ public_class_method :new, :digest, :hexdigest
10
+ end
11
+ end
data/lib/hmac-sha2.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'hmac'
2
+ require 'digest/sha2'
3
+
4
+ module HMAC
5
+ class SHA256 < Base
6
+ def initialize(key = nil)
7
+ super(Digest::SHA256, 64, 32, key)
8
+ end
9
+ public_class_method :new, :digest, :hexdigest
10
+ end
11
+
12
+ class SHA384 < Base
13
+ def initialize(key = nil)
14
+ super(Digest::SHA384, 128, 48, key)
15
+ end
16
+ public_class_method :new, :digest, :hexdigest
17
+ end
18
+
19
+ class SHA512 < Base
20
+ def initialize(key = nil)
21
+ super(Digest::SHA512, 128, 64, key)
22
+ end
23
+ public_class_method :new, :digest, :hexdigest
24
+ end
25
+ end
data/lib/hmac.rb ADDED
@@ -0,0 +1,112 @@
1
+ # Copyright (C) 2001 Daiki Ueno <ueno@unixuser.org>
2
+ # This library is distributed under the terms of the Ruby license.
3
+
4
+ # This module provides common interface to HMAC engines.
5
+ # HMAC standard is documented in RFC 2104:
6
+ #
7
+ # H. Krawczyk et al., "HMAC: Keyed-Hashing for Message Authentication",
8
+ # RFC 2104, February 1997
9
+ #
10
+ # These APIs are inspired by JCE 1.2's javax.crypto.Mac interface.
11
+ #
12
+ # <URL:http://java.sun.com/security/JCE1.2/spec/apidoc/javax/crypto/Mac.html>
13
+
14
+ module HMAC
15
+ class Base
16
+ def initialize(algorithm, block_size, output_length, key)
17
+ @algorithm = algorithm
18
+ @block_size = block_size
19
+ @output_length = output_length
20
+ @status = STATUS_UNDEFINED
21
+ @key_xor_ipad = ''
22
+ @key_xor_opad = ''
23
+ set_key(key) unless key.nil?
24
+ end
25
+
26
+ private
27
+ def check_status
28
+ unless @status == STATUS_INITIALIZED
29
+ raise RuntimeError,
30
+ "The underlying hash algorithm has not yet been initialized."
31
+ end
32
+ end
33
+
34
+ public
35
+ def set_key(key)
36
+ # If key is longer than the block size, apply hash function
37
+ # to key and use the result as a real key.
38
+ key = @algorithm.digest(key) if key.size > @block_size
39
+ key_xor_ipad = "\x36" * @block_size
40
+ key_xor_opad = "\x5C" * @block_size
41
+ for i in 0 .. key.size - 1
42
+ key_xor_ipad[i] ^= key[i]
43
+ key_xor_opad[i] ^= key[i]
44
+ end
45
+ @key_xor_ipad = key_xor_ipad
46
+ @key_xor_opad = key_xor_opad
47
+ @md = @algorithm.new
48
+ @status = STATUS_INITIALIZED
49
+ end
50
+
51
+ def reset_key
52
+ @key_xor_ipad.gsub!(/./, '?')
53
+ @key_xor_opad.gsub!(/./, '?')
54
+ @key_xor_ipad[0..-1] = ''
55
+ @key_xor_opad[0..-1] = ''
56
+ @status = STATUS_UNDEFINED
57
+ end
58
+
59
+ def update(text)
60
+ check_status
61
+ # perform inner H
62
+ md = @algorithm.new
63
+ md.update(@key_xor_ipad)
64
+ md.update(text)
65
+ str = md.digest
66
+ # perform outer H
67
+ md = @algorithm.new
68
+ md.update(@key_xor_opad)
69
+ md.update(str)
70
+ @md = md
71
+ end
72
+ alias << update
73
+
74
+ def digest
75
+ check_status
76
+ @md.digest
77
+ end
78
+
79
+ def hexdigest
80
+ check_status
81
+ @md.hexdigest
82
+ end
83
+ alias to_s hexdigest
84
+
85
+ # These two class methods below are safer than using above
86
+ # instance methods combinatorially because an instance will have
87
+ # held a key even if it's no longer in use.
88
+ def Base.digest(key, text)
89
+ begin
90
+ hmac = self.new(key)
91
+ hmac.update(text)
92
+ hmac.digest
93
+ ensure
94
+ hmac.reset_key
95
+ end
96
+ end
97
+
98
+ def Base.hexdigest(key, text)
99
+ begin
100
+ hmac = self.new(key)
101
+ hmac.update(text)
102
+ hmac.hexdigest
103
+ ensure
104
+ hmac.reset_key
105
+ end
106
+ end
107
+
108
+ private_class_method :new, :digest, :hexdigest
109
+ end
110
+
111
+ STATUS_UNDEFINED, STATUS_INITIALIZED = 0, 1
112
+ end
@@ -0,0 +1,109 @@
1
+ require 'openid/util'
2
+
3
+ module OpenID
4
+
5
+ # Represents an "association" between a consumer and server, and
6
+ # is also used for storage of the information exchanged
7
+ # during the openid.mode='associate' transaction.
8
+ # This class is used by the both the server and consumer.
9
+ class Association
10
+ @@version = '2'
11
+ @@assoc_keys = [
12
+ 'version',
13
+ 'handle',
14
+ 'secret',
15
+ 'issued',
16
+ 'lifetime',
17
+ 'assoc_type'
18
+ ]
19
+
20
+ attr_reader :handle, :secret, :issued, :lifetime, :assoc_type
21
+
22
+ def Association.from_expires_in(expires_in, handle, secret, assoc_type)
23
+ issued = Time.now.to_i
24
+ lifetime = expires_in
25
+ new(handle, secret, issued, lifetime, assoc_type)
26
+ end
27
+
28
+ def Association.serialize(assoc)
29
+ data = [
30
+ '2',
31
+ assoc.handle,
32
+ OpenID::Util.to_base64(assoc.secret),
33
+ assoc.issued.to_i.to_s,
34
+ assoc.lifetime.to_i.to_s,
35
+ assoc.assoc_type
36
+ ]
37
+
38
+ lines = ""
39
+ (0...@@assoc_keys.length).collect do |i|
40
+ lines += "#{@@assoc_keys[i]}: #{data[i]}\n"
41
+ end
42
+
43
+ lines
44
+ end
45
+
46
+ def Association.deserialize(assoc_s)
47
+ keys = []
48
+ values = []
49
+ assoc_s.split("\n").each do |line|
50
+ k, v = line.split(":", 2)
51
+ keys << k.strip
52
+ values << v.strip
53
+ end
54
+
55
+ version, handle, secret, issued, lifetime, assoc_type = values
56
+ raise 'VersionError' if version != @@version
57
+
58
+ secret = OpenID::Util.from_base64(secret)
59
+ issued = issued.to_i
60
+ lifetime = lifetime.to_i
61
+ Association.new(handle, secret, issued, lifetime, assoc_type)
62
+ end
63
+
64
+ def initialize(handle, secret, issued, lifetime, assoc_type)
65
+ if assoc_type != 'HMAC-SHA1'
66
+ raise ArgumentError, "HMAC-SHA1 is the only supported assoc_type, got #{assoc_type}"
67
+ end
68
+
69
+ @handle = handle
70
+ @secret = secret
71
+ @issued = issued
72
+ @lifetime = lifetime
73
+ @assoc_type = assoc_type
74
+ end
75
+
76
+ def expires_in
77
+ [0, @issued + @lifetime - Time.now.to_i].max
78
+ end
79
+
80
+ def expired?
81
+ return expires_in == 0
82
+ end
83
+
84
+ def sign(pairs)
85
+ kv = ''
86
+ pairs.each {|k,v| kv << "#{k}:#{v}\n"}
87
+ return OpenID::Util.hmac_sha1(@secret, kv)
88
+ end
89
+
90
+ def sign_hash(fields, hash, prefix='openid.')
91
+ pairs = []
92
+ fields.each { |f| pairs << [f, hash[prefix+f]] }
93
+ return OpenID::Util.to_base64(sign(pairs))
94
+ end
95
+
96
+ def add_signature(fields, hash, prefix='openid.')
97
+ sig = sign_hash(fields, hash, prefix)
98
+ signed = fields.join(',')
99
+ hash[prefix+'sig'] = sig
100
+ hash[prefix+'signed'] = signed
101
+ end
102
+
103
+ def ==(other)
104
+ self.instance_variable_hash == other.instance_variable_hash
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,928 @@
1
+ require "uri"
2
+
3
+ require "openid/util"
4
+ require "openid/dh"
5
+ require "openid/fetchers"
6
+ require "openid/association"
7
+ require "openid/discovery"
8
+
9
+
10
+ # Everything in this library exists within the OpenID Module. Users of
11
+ # the library should look at OpenID::Consumer and/or OpenID::Server
12
+ module OpenID
13
+
14
+ # ==Overview
15
+ #
16
+ # Brief terminology:
17
+ #
18
+ # [+Consumer+]
19
+ # The website wanting to verify an OpenID identity URL. Sometimes
20
+ # called a "relying party". If you want people to log into your site
21
+ # using OpenID, then you are the consumer.
22
+ #
23
+ # [+Server+]
24
+ # The website which makes assertions as to whether or not the user
25
+ # at the end of the browser owns the URL they say they do.
26
+ #
27
+ # [+Redirect+]
28
+ # An HTTP 302 (Temporarily Moved) redirect. When issued as an HTTP
29
+ # response from the server, the browser changes it's location to the
30
+ # value specified.
31
+ #
32
+ # The OpenID authentication process requires the following steps,
33
+ # as visible to the user of this library:
34
+ #
35
+ # 1. The user enters their OpenID into a field on the consumer's
36
+ # site, and hits a login button.
37
+ #
38
+ # 2. The consumer site discovers the user's OpenID server information using
39
+ # the Yadis protocol (Potentially falling back to OpenID 1.0 "linkrel"
40
+ # discovery).
41
+ #
42
+ # 3. The consumer site prepares a URL to be sent to the server
43
+ # which contains the OpenID autentication information, and
44
+ # issues a redirect user's browser.
45
+ #
46
+ # 4. The server then verifies that the user owns the URL
47
+ # provided, and sends the browser a redirect
48
+ # back to the consumer. This redirect contains the
49
+ # server's response to the authentication request.
50
+ #
51
+ # The most important part of the flow to note is the consumer's site
52
+ # must handle two separate HTTP requests in order to perform the
53
+ # full identity check. These two HTTP requests are described in
54
+ # steps 1 and 4 above, and are handled by Consumer.begin and
55
+ # Consumer.complete respectively.
56
+ #
57
+ #
58
+ # ==Consumer Library Design
59
+ #
60
+ # The library is designed with the above flow in mind. The
61
+ # goal is to make it as easy as possible to perform the above steps
62
+ # securely.
63
+ #
64
+ # At a high level, there are two important parts in the consumer
65
+ # library. The first important part is the OpenID::Consumer class,
66
+ # which contains the public interface to the consumer logic.
67
+ # The second is the OpenID::Store class, which defines the
68
+ # interface needed to store the state the consumer needs to maintain
69
+ # between requests.
70
+ #
71
+ # In general, the second part is less important for users of the
72
+ # library to know about, as several concrete store implementations are
73
+ # provided. The user simply needs to choose the store which best fits
74
+ # their environment and requirements.
75
+ #
76
+ #
77
+ # ==Stores and Dumb Mode
78
+ #
79
+ # OpenID is a protocol that works best when the consumer site is
80
+ # able to store some state. This is the normal mode of operation
81
+ # for the protocol, and is sometimes referred to as smart mode.
82
+ # There is also a fallback mode, known as dumb mode, which is
83
+ # available when the consumer site is not able to store state. This
84
+ # mode should be avoided when possible, as it leaves the
85
+ # implementation more vulnerable to replay attacks.
86
+ #
87
+ # The mode the library works in for normal operation is determined
88
+ # by the store that it is given. The store is an abstraction that
89
+ # handles the data that the consumer needs to manage between HTTP
90
+ # requests in order to operate efficiently and securely.
91
+ #
92
+ # Several store implementation are provided, and the interface is
93
+ # fully documented so that custom stores can be used as well. The
94
+ # implementations that are provided allow the consumer site to store
95
+ # data in a couple of different ways: in the filesystem,
96
+ # or in an SQL database.
97
+ #
98
+ # There is an additional concrete store provided that puts the
99
+ # consumer in dumb mode. This is not recommended, as it removes the
100
+ # library's ability to stop replay attacks reliably. It still uses
101
+ # time-based checking to make replay attacks only possible within a
102
+ # small window, but they remain possible within that window. This
103
+ # store should only be used if the consumer site has no way to
104
+ # retain data between requests at all. See DumbStore for more info.
105
+ #
106
+ # If your ennvironment permits, use of the FilesystemStore
107
+ # is recommended.
108
+ #
109
+ #
110
+ # ==Immediate Mode
111
+ #
112
+ # If you are new to OpenID, it is suggested that you skip this section
113
+ # and refer to it later. Immediate mode is an advanced consumer topic.
114
+ #
115
+ # In the flow described in the overview, the user may need to confirm to the
116
+ # identity server that it's ok to authorize his or her identity.
117
+ # The server may draw pages asking for information from the user
118
+ # before it redirects the browser back to the consumer's site. This
119
+ # is generally transparent to the consumer site, so it is typically
120
+ # ignored as an implementation detail.
121
+ #
122
+ # There can be times, however, where the consumer site wants to get
123
+ # a response immediately. When this is the case, the consumer can
124
+ # put the library in immediate mode. In immediate mode, there is an
125
+ # extra response possible from the server, which is essentially the
126
+ # server reporting that it doesn't have enough information to answer
127
+ # the question yet. In addition to saying that, the identity server
128
+ # provides a URL to which the user can be sent to provide the needed
129
+ # information and let the server finish handling the original
130
+ # request.
131
+ #
132
+ # You may invoke immediate mode when building the redirect URL to the
133
+ # OpenID server in the SuccessRequest.redirect_url method. Pass true
134
+ # for the +immediate+ paramter. Read the interface for Consumer.complete
135
+ # for information about handling the additional response.
136
+ #
137
+ # ==Using the Library
138
+ #
139
+ # Integrating this library into an application is a
140
+ # relatively straightforward process. The process usually follows this plan:
141
+ #
142
+ # Add an OpenID login field somewhere on your site. When an OpenID
143
+ # is entered in that field and the form is submitted, it should make
144
+ # a request to the site which includes that OpenID URL.
145
+ #
146
+ # When your site receives that request, it should create an
147
+ # OpenID::Consumer instance, and call
148
+ # OpenID::Consumer.begin. If begin completes successfully,
149
+ # it will return a SuccessRequest object. Otherwise it will subclass
150
+ # of OpenIDStatus which contains additional information about the
151
+ # the failure.
152
+ #
153
+ # If successful, build a redirect URL to the server by calling
154
+ # SuccessRequest.redirect_url and send back an HTTP 302 redirect
155
+ # of that URL to the user's browser. The redirect_url accepts a
156
+ # return_to parameter, which is the URL to which they will return
157
+ # to fininsh the OpenID transaction. This URL is supplied by you,
158
+ # and should be able to handle step 4 of the flow described in the
159
+ # overview.
160
+ #
161
+ # That's the first half of the authentication process. The second
162
+ # half of the process is done after the OpenID server sends the
163
+ # user's browser a redirect back to your site with the
164
+ # authentication response.
165
+ #
166
+ # When that happens, the browser will make a request to the return_to
167
+ # URL you provided to the SuccessRequest.redirect_url
168
+ # method. The request will have several query parameters added
169
+ # to the URL by the identity server as the information necessary to
170
+ # finish the request.
171
+ #
172
+ # Your job here is to make sure that the action performed at the return_to
173
+ # URL creates an instnce of OpenID::Consumer, and calls the Consumer.complete
174
+ # method. This call will
175
+ # return a SuccessResponse object, or a subclass of OpenIDStatus explaining,
176
+ # the failure. See the documentation for Consumer.complete
177
+ # for a full explanation of the possible responses.
178
+ #
179
+ # If you received a SuccessResponse, you may access the identity URL
180
+ # of the user though it's +identity_url+ method.
181
+ class Consumer
182
+
183
+ @@token_key = '_openid_consumer_token'
184
+ @@disco_suffix = 'xopenid_services'
185
+ attr_accessor :consumer, :session, :fetcher
186
+
187
+ # Creates a new OpenID::Consumer instance. You should create a new
188
+ # instance of the Consumer object with every HTTP request that handles
189
+ # OpenID transactions. Do not store the instance of it in a
190
+ # global variable somewhere.
191
+ #
192
+ # [+session+]
193
+ # A hash-like object representing the user's session data. This is
194
+ # used for keeping state of the OpenID transaction when the user is
195
+ # redirected to the server. In a rails application, the controller's
196
+ # @session instance variable should be used.
197
+ #
198
+ # [+store+]
199
+ # This must be an object that implements the OpenID::Store interface.
200
+ # Several concrete implementations are provided, to cover
201
+ # most common use cases. We recommend using the simple file based
202
+ # store bundled with the library: OpenID::FilesystemStore.
203
+ #
204
+ # [+fetcher+]
205
+ # Optional. If provided, this must be an instance that implements
206
+ # OpenID::Fetcher interface. If no fetcher is provided,
207
+ # an OpenID::StandardFetcher instance will be created
208
+ # for you automatically. If you need custom fetcher behavior, it
209
+ # is probably best to subclass StandardFetcher, and pass your instance
210
+ # in here.
211
+ #
212
+ # This object keeps an internal instance of OpenID::GenericConsumer
213
+ # for low level OpenID calls, called +consumer+. You may use a custom
214
+ # certificate authority PEM file for veryifying HTTPS server certs
215
+ # by calling the GenericConsumer.ca_path= method of the +consumer+
216
+ # instance variable.
217
+ def initialize(session, store, fetcher=nil)
218
+ @session = session
219
+ @consumer = GenericConsumer.new(store, fetcher)
220
+ end
221
+
222
+ # +begin+ is called to start the OpenID verification process. See steps
223
+ # 1-2 in the overview at the top of this file.
224
+ #
225
+ # ==Parameters
226
+ #
227
+ # [+user_url+]
228
+ # Identity URL given by the user. +begin+ performs a textual
229
+ # transformation of the URL to try and make sure it is "normalized",
230
+ # for example, a user_url of example.com will be normalized to
231
+ # http://example.com/ normalizing and resolving any redirects
232
+ # the server might issue.
233
+ #
234
+ # ==Return Value
235
+ #
236
+ # +begin+ returns a subclass of OpenIDStatus, which is an object
237
+ # that has a +status+ method. The status methodfor this object will either
238
+ # return OpenID::SUCCESS, or OpenID::FAILURE. Generally +begin+ will fail
239
+ # if the users' OpenID page cannot be retrieved or OpenID server
240
+ # information cannot be determined.
241
+ #
242
+ # ===Success
243
+ #
244
+ # In the case that request.status equals OpenID::SUCCESS, the response
245
+ # will be of type OpenID::SuccessRequest. The SuccessRequest object
246
+ # may the be used to add simple registration extension arguments,
247
+ # using SuccessRequest.add_extension_arg, and build the redirect
248
+ # url to the server using SuccessRequest.redirect_url as described
249
+ # in step 3 of the overview.
250
+ #
251
+ # The next step in the success case is to actually build the redirect
252
+ # URL to the server. Please see the documentation for
253
+ # SuccessRequest.redirect_url for details. Once the redirect url
254
+ # is created, you should issue an HTTP 302 temporary redirect to the
255
+ # user's browser, sending her to the OpenID server. Once the user
256
+ # finishes the operations on the server, she will be redirected back to
257
+ # the return_to URL you passed to redirect_url, which should invoke
258
+ # the Consumer.complete method.
259
+ #
260
+ # ===Failure
261
+ #
262
+ # If the library is unable to fetch the +user_url+, or no server
263
+ # information can be determined, or if the server information is malformed,
264
+ # +begin+ will return a FailureRequest object. The status method of this
265
+ # object will return OpenID::FAILURE. FailureRequest objects have a
266
+ # +msg+ method which provides more detailed information as to why
267
+ # the request failed.
268
+ def begin(user_url)
269
+ user_url = OpenID::Util.normalize_url(user_url)
270
+ unless user_url
271
+ user_url = user_url.to_s
272
+ return FailureRequest.new("Invalid URL: #{user_url}")
273
+ end
274
+
275
+ discovery = self.get_discovery(user_url)
276
+ service = discovery.next_service
277
+
278
+ if service.nil?
279
+ return FailureRequest.new('No service endpoints found.')
280
+ end
281
+
282
+ return self.begin_without_discovery(service)
283
+ end
284
+
285
+ # Start the OpenID transaction without doing OpenID server
286
+ # discovery. This method is used internally by Consumer.begin after
287
+ # discovery is performed, and exists to provide an interface for library
288
+ # users needing to perform their own discovery.
289
+ #
290
+ # ==Parameters
291
+ #
292
+ # +service+ must be an OpenID::OpenIDServiceEnpoint object, or an object
293
+ # that implements it's interface. You may produce these objects
294
+ # and perform discovery manually using OpenID::OpenIDDiscovery.
295
+ #
296
+ # ==Return Value
297
+ #
298
+ # +begin_without_discovery+ always returns an OpenID::SuccessRequest
299
+ # object. Please see the success documentation for OpenID::Consumer.begin
300
+ # for more information.
301
+ def begin_without_discovery(service)
302
+ request = @consumer.begin(service)
303
+ @session[@@token_key] = request.token
304
+ return request
305
+ end
306
+
307
+ # Called to interpret the server's response to an OpenID request. It
308
+ # is called in step 4 of the flow described in the consumer overview.
309
+ #
310
+ # ==Parameters
311
+ # [+query+]
312
+ # A hash of the query paramters for this HTTP request.
313
+ #
314
+ # ==Return Value
315
+ # Return value is a subclass of OpenIDStatus, and may have a status
316
+ # of OpenID::SUCCESS, OpenID::CANCEL, OpenID::FAILURE,
317
+ # or OpenID::SETUP_NEEDED. The status may be accessed through the
318
+ # +status+ method of the response object.
319
+ #
320
+ # When OpenID::SUCCESS is returned, the response object will be of
321
+ # type SuccessResponse, which has several useful attributes including
322
+ # +identity_url+, +service+, and a method +extension_response+ for
323
+ # extracting potential signed extension reponses from the server. See
324
+ # the documentation for OpenID::SuccessResponse for more information
325
+ # about it's interface and methods.
326
+ #
327
+ # In the case of response.status being OpenID::CANCEL, the user cancelled
328
+ # the OpenID transaction on the server. The response will be an instance
329
+ # of OpenID::CancelResponse, and you may access the originally submitted
330
+ # identity URL and service information through that object.
331
+ #
332
+ # When status is OpenID::FAILURE, the object will be an instance of
333
+ # OpenID::FailureResponse. If the identity which failed can be determined
334
+ # it will be available by accessing the +identity_url+ attribute of the
335
+ # response. FailureResponse objects also have a +msg+ attribute
336
+ # which may be useful in debugging. If no msg is specified, msg will be
337
+ # nil.
338
+ #
339
+ # When OpenID::SETUP_NEEDED is returned, the response object is an
340
+ # instance of OpenID::SetupNeededResponse. The useful piece of information
341
+ # contained in this response is the +setup_url+ method, which
342
+ # should be used to send the user to the server and log in.
343
+ # This response is only generated by immediate
344
+ # mode requests (openid.mode=immediate). The user should be redirected
345
+ # in to the +setup_url+, either in the current window or in a
346
+ # new browser window.
347
+ def complete(query)
348
+ begin
349
+ token = @session.delete(@@token_key)
350
+ rescue
351
+ token = @session[@@token_key]
352
+ @session[@@token_key] = nil
353
+ end
354
+
355
+ if token.nil?
356
+ resp = FailureResponse.new(nil, 'No session state found.')
357
+ else
358
+ resp = @consumer.complete(query, token)
359
+ end
360
+
361
+ disco = self.get_discovery(resp.identity_url)
362
+
363
+ if [SUCCESS, CANCEL].member?(resp.status)
364
+ if resp.identity_url
365
+ resp.service = disco.finish
366
+ end
367
+ else
368
+ resp.service = disco.current
369
+ end
370
+
371
+ return resp
372
+ end
373
+
374
+ protected
375
+
376
+ # Used internally to create an instnace of the OpenIDDiscovery object.
377
+ def get_discovery(url)
378
+ return OpenIDDiscovery.new(@session, url, @consumer.fetcher,
379
+ @@disco_suffix)
380
+ end
381
+
382
+ end
383
+
384
+ # This class implements the common logic for OpenID consumers. It
385
+ # is used by the higher level OpenID::Consumer class. Only advanced
386
+ # users with special needs should ever have to look at this class.
387
+ #
388
+ # The only part of the library which has to be used and isn't
389
+ # documented in full here is the store required to create an
390
+ # OpenID::Consumer instance. More on the abstract store type and
391
+ # concrete implementations of it that are provided in the documentation
392
+ # of OpenID::Consumer.new
393
+ class GenericConsumer
394
+
395
+ # Number of characters to be used in generated nonces
396
+ @@NONCE_LEN = 8
397
+
398
+ # Nonce character set
399
+ @@NONCE_CHRS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
400
+ # Number of seconds the tokens generated by this library will be valid for.
401
+ @@TOKEN_LIFETIME = 60 * 2
402
+
403
+ @@D_SUFFIX = 'openid_disco'
404
+
405
+ attr_reader :fetcher
406
+
407
+ public
408
+
409
+ # Creates a new Consumer instance. You should create a new
410
+ # instance of the Consumer object with every HTTP request. Do not
411
+ # store the instance of it in a global variable somewhere.
412
+ #
413
+ # [+store+]
414
+ # This must be an object that implements the OpenID::Store interface.
415
+ # Several concrete implementations are provided, to cover
416
+ # most common use cases. We recommend using the simple file based
417
+ # store bundled with the library: OpenID::FilesystemStore.
418
+ #
419
+ # [+fetcher+]
420
+ # Optional. If provided, this must be an instance that implements
421
+ # Fetcher interface. If no fetcher is provided,
422
+ # an instance of OpenID::StandardFetcher will be created for
423
+ # you automatically.
424
+ def initialize(store, fetcher=nil)
425
+ if fetcher.nil?
426
+ fetcher = StandardFetcher.new
427
+ end
428
+ @store = store
429
+ @fetcher = fetcher
430
+ @ca_path = nil
431
+ end
432
+
433
+ # Set the path to a pem certificate authority file for verifying
434
+ # server certificates during HTTPS. If you are interested in verifying
435
+ # certs like the mozilla web browser, have a look at the files here:
436
+ #
437
+ # http://curl.haxx.se/docs/caextract.html
438
+ def ca_path=(ca_path)
439
+ ca_path = ca_path.to_s
440
+ if File.exists?(ca_path)
441
+ @ca_path = ca_path
442
+ @fetcher.ca_path = ca_path
443
+ else
444
+ raise ArgumentError, "#{ca_path} is not a valid file path"
445
+ end
446
+ end
447
+
448
+ # See the interface documentation for Consumer.begin_without_discovery
449
+ # for arugumnets and return values of this method.
450
+ # begin_without_discovery is a light wrapper around this method, and the
451
+ # has the same interface.
452
+ def begin(service)
453
+ nonce = self.create_nonce
454
+ token = self.gen_token(service.consumer_id, service.server_id,
455
+ service.server_url)
456
+
457
+ assoc = self.get_association(service.server_url)
458
+ return SuccessRequest.new(assoc, token, nonce, service)
459
+ end
460
+
461
+ # Please see the interface docs for Consumer.complete. This method accpets
462
+ # a +token+ paramter which is provided though the SuccessRequest object
463
+ # generated in the +begin+ call. The token should be stored somewhere
464
+ # in the user's session or environment and passed into this method
465
+ # along with the full query string Hash. Consumer.complete has the
466
+ # full list of return values for this method.
467
+ def complete(query, token)
468
+ # get the service data out of the token
469
+ pieces = self.split_token(token)
470
+ if pieces
471
+ consumer_id, server_id, server_url = pieces
472
+ else
473
+ return FailureResponse.new(nil, msg='bad token')
474
+ end
475
+
476
+ # get the nonce out of the query
477
+ nonce = query['nonce']
478
+ if nonce.nil?
479
+ return FailureResponse.new(consumer_id, 'could not extract nonce')
480
+ end
481
+
482
+ mode = query["openid.mode"]
483
+
484
+ case mode
485
+ when "cancel"
486
+ return CancelResponse.new(consumer_id)
487
+
488
+ when "error"
489
+ error = query["openid.error"]
490
+ unless error.nil?
491
+ OpenID::Util.log('Error: '+error)
492
+ end
493
+ return FailureResponse.new(nil, msg=error)
494
+
495
+ when "id_res"
496
+ return self.do_id_res(nonce, consumer_id, server_id, server_url, query)
497
+
498
+ else
499
+ return FailureResponse.new(nil, msg="unknown mode #{mode}")
500
+ end
501
+
502
+ end
503
+
504
+ # Low level method for handling the OpenID server response.
505
+ def do_id_res(nonce, consumer_id, server_id, server_url, query)
506
+ user_setup_url = query["openid.user_setup_url"]
507
+ if user_setup_url
508
+ return SetupNeededResponse.new(user_setup_url)
509
+ end
510
+
511
+ return_to = query["openid.return_to"]
512
+ server_id2 = query["openid.identity"]
513
+ assoc_handle = query["openid.assoc_handle"]
514
+
515
+ if return_to.nil?
516
+ return FailureResponse.new(consumer_id, msg='openid.return_to was nil')
517
+ elsif server_id2.nil?
518
+ return FailureResponse.new(consumer_id, msg='openid.identity was nil')
519
+ elsif assoc_handle.nil?
520
+ return FailureResponse.new(consumer_id, msg='openid.assoc_handle was nil')
521
+ end
522
+
523
+ if server_id != server_id2
524
+ return FailureResponse.new(consumer_id, msg='server ids do not match')
525
+ end
526
+
527
+ assoc = @store.get_association(server_url, assoc_handle)
528
+
529
+ if assoc.nil?
530
+ # It's not an association we know about. Dumb mode is our
531
+ # only possible path for recovery.
532
+ code = self.check_auth(nonce, query, server_url)
533
+ if code == SUCCESS
534
+ return SuccessResponse.new(consumer_id, query)
535
+ else
536
+ return FailureResponse.new(consumer_id, 'check_auth failed')
537
+ end
538
+ end
539
+
540
+ if assoc.expires_in <= 0
541
+ OpenID::Util.log("Association with #{server_url} expired")
542
+ FailureResponse.new(consumer_id, 'assoc expired')
543
+ end
544
+
545
+ # Check the signature
546
+ sig = query["openid.sig"]
547
+ return FailureResponse.new(consumer_id, 'no sig') if sig.nil?
548
+ signed = query["openid.signed"]
549
+ return FailureResponse.new(consumer_id, 'no signed') if signed.nil?
550
+
551
+ args = OpenID::Util.get_openid_params(query)
552
+ signed_list = signed.split(",")
553
+ _signed, v_sig = OpenID::Util.sign_reply(args, assoc.secret, signed_list)
554
+
555
+ if v_sig != sig
556
+ return FailureResponse.new(consumer_id, 'sig mismatch')
557
+ end
558
+
559
+ unless @store.use_nonce(nonce)
560
+ return FailureResponse.new(consumer_id, 'nonce already used')
561
+ end
562
+
563
+ return SuccessResponse.new(consumer_id, query)
564
+ end
565
+
566
+ # Low level method for performing OpenID check_authenticaion requests
567
+ def check_auth(nonce, query, server_url)
568
+ check_args = OpenID::Util.get_openid_params(query)
569
+ check_args["openid.mode"] = "check_authentication"
570
+ post_data = OpenID::Util.urlencode(check_args)
571
+
572
+ ret = @fetcher.post(server_url, post_data)
573
+ if ret.nil?
574
+ return FAILURE
575
+ else
576
+ url, body = ret
577
+ end
578
+
579
+ results = OpenID::Util.parsekv(body)
580
+ is_valid = results.fetch("is_valid", "false")
581
+
582
+ if is_valid == "true"
583
+ invalidate_handle = results["invalidate_handle"]
584
+ unless invalidate_handle.nil?
585
+ @store.remove_association(server_url, invalidate_handle)
586
+ end
587
+ unless @store.use_nonce(nonce)
588
+ return FAILURE
589
+ end
590
+ return SUCCESS
591
+ end
592
+
593
+ error = results["error"]
594
+ return FAILURE unless error.nil?
595
+ return FAILURE
596
+ end
597
+
598
+ # Create a nonce and store it for preventing replace attacks.
599
+ def create_nonce
600
+ # build the nonce and store it
601
+ nonce = OpenID::Util.random_string(@@NONCE_LEN, @@NONCE_CHRS)
602
+ @store.store_nonce(nonce)
603
+ return nonce
604
+ end
605
+
606
+ # Get existing or create a new association (shared secret) with the
607
+ # server at +server_url+.
608
+ def get_association(server_url)
609
+ return nil if @store.dumb?
610
+ assoc = @store.get_association(server_url)
611
+ return assoc unless assoc.nil?
612
+ return self.associate(server_url)
613
+ end
614
+
615
+ # Generate a token representing the user and their server. Used
616
+ # internally for maintaining cross request state.
617
+ def gen_token(consumer_id, server_id, server_url)
618
+ timestamp = Time.now.to_i.to_s
619
+ joined = [timestamp, consumer_id, server_id, server_url].join("\x00")
620
+ sig = OpenID::Util.hmac_sha1(@store.get_auth_key, joined)
621
+ OpenID::Util.to_base64(sig+joined)
622
+ end
623
+
624
+ # Extract server and user information from a token string.
625
+ def split_token(token)
626
+ return nil if token.nil?
627
+
628
+ token = OpenID::Util.from_base64(token)
629
+ return nil if token.length < 20
630
+
631
+ sig, joined = token[(0...20)], token[(20...token.length)]
632
+ return nil if OpenID::Util.hmac_sha1(@store.get_auth_key, joined) != sig
633
+
634
+ s = joined.split("\x00")
635
+ return nil if s.length != 4
636
+
637
+ timestamp, consumer_id, server_id, server_url = s
638
+
639
+ timestamp = timestamp.to_i
640
+ return nil if timestamp == 0
641
+ return nil if (timestamp + @@TOKEN_LIFETIME) < Time.now.to_i
642
+
643
+ return [consumer_id, server_id, server_url].freeze
644
+ end
645
+
646
+ # Make the openid.associate call to the server.
647
+ def associate(server_url)
648
+ dh = OpenID::DiffieHellman.new
649
+ cpub = OpenID::Util.to_base64(OpenID::Util.num_to_str(dh.public))
650
+ args = {
651
+ 'openid.mode' => 'associate',
652
+ 'openid.assoc_type' =>'HMAC-SHA1',
653
+ 'openid.session_type' =>'DH-SHA1',
654
+ 'openid.dh_modulus' => OpenID::Util.to_base64(OpenID::Util.num_to_str(dh.p)),
655
+ 'openid.dh_gen' => OpenID::Util.to_base64(OpenID::Util.num_to_str(dh.g)),
656
+ 'openid.dh_consumer_public' => cpub
657
+ }
658
+ body = OpenID::Util.urlencode(args)
659
+
660
+ ret = @fetcher.post(server_url, body)
661
+ return nil if ret.nil?
662
+ url, data = ret
663
+ results = OpenID::Util.parsekv(data)
664
+
665
+ assoc_type = results["assoc_type"]
666
+ return nil if assoc_type.nil? or assoc_type != "HMAC-SHA1"
667
+
668
+ assoc_handle = results["assoc_handle"]
669
+ return nil if assoc_handle.nil?
670
+
671
+ expires_in = results.fetch("expires_in", "0").to_i
672
+
673
+ session_type = results["session_type"]
674
+ if session_type.nil?
675
+ secret = OpenID::Util.from_base64(results["mac_key"])
676
+ else
677
+ return nil if session_type != "DH-SHA1"
678
+
679
+ dh_server_public = results["dh_server_public"]
680
+ return nil if dh_server_public.nil?
681
+
682
+ spub = OpenID::Util.str_to_num(OpenID::Util.from_base64(dh_server_public))
683
+ dh_shared = dh.get_shared_secret(spub)
684
+ enc_mac_key = results["enc_mac_key"]
685
+ secret = OpenID::Util.strxor(OpenID::Util.from_base64(enc_mac_key),
686
+ OpenID::Util.sha1(OpenID::Util.num_to_str(dh_shared)))
687
+ end
688
+
689
+ assoc = OpenID::Association.from_expires_in(expires_in, assoc_handle,
690
+ secret, 'HMAC-SHA1')
691
+ @store.store_association(server_url, assoc)
692
+ return assoc
693
+ end
694
+
695
+ end
696
+
697
+ # Base class for objects returned from Consumer.begin and Consumer.complete
698
+ class OpenIDStatus
699
+
700
+ attr_reader :status
701
+
702
+ def initialize(status)
703
+ @status = status
704
+ end
705
+
706
+ end
707
+
708
+ # Returned by Consumer.begin when server information cannot be determined
709
+ # from the provided identity URL. The +msg+ method may return a useful
710
+ # string for debugging the request.
711
+ class FailureRequest < OpenIDStatus
712
+
713
+ attr_reader :msg
714
+
715
+ def initialize(msg='')
716
+ super(FAILURE)
717
+ @msg = msg
718
+ end
719
+
720
+ end
721
+
722
+ # Encapsulates the information the library retrieves and uses during
723
+ # Consumer.begin.
724
+ class SuccessRequest < OpenIDStatus
725
+
726
+ attr_reader :token, :server_id, :server_url, :nonce, :identity_url, \
727
+ :service, :return_to_args
728
+
729
+ # Creates a new SuccessRequest object. This just stores each
730
+ # argument in an appropriately named field.
731
+ #
732
+ # Users of this library should not create instances of this
733
+ # class. Instances of this class are created by Consumer
734
+ # during begin.
735
+ def initialize(assoc, token, nonce, service)
736
+ super(SUCCESS)
737
+ @service = service
738
+ @server_id = service.server_id
739
+ @server_url = service.server_url
740
+ @identity_url = service.consumer_id
741
+ @extra_args = {}
742
+ @return_to_args = {'nonce' => nonce}
743
+
744
+ @assoc = assoc
745
+ @token = token
746
+ @nonce = nonce
747
+ end
748
+
749
+ # Called to construct the redirect URL sent to
750
+ # the browser to ask the server to verify its identity. This is
751
+ # called in step 3 of the flow described in the overview.
752
+ # Please note that you don't need to call this method directly
753
+ # unless you need to create a custom redirect, as it is called
754
+ # directly during begin. The generated redirect should be
755
+ # sent to the browser which initiated the authorization request.
756
+ #
757
+ # ==Parameters
758
+ # [+trust_root+]
759
+ # This is a URL that will be sent to the
760
+ # server to identify this site. The OpenID spec (
761
+ # http://www.openid.net/specs.bml#mode-checkid_immediate )
762
+ # has more information on what the trust_root value is for
763
+ # and what its form can be. While the trust root is
764
+ # officially optional in the OpenID specification, this
765
+ # implementation requires that it be set. Nothing is
766
+ # actually gained by leaving out the trust root, as you can
767
+ # get identical behavior by specifying the return_to URL as
768
+ # the trust root.
769
+ #
770
+ # [+return_to+]
771
+ # This is the URL that will be included in the
772
+ # generated redirect as the URL the OpenID server will send
773
+ # its response to. The URL passed in must handle OpenID
774
+ # authentication responses.
775
+ #
776
+ # [+immediate+]
777
+ # Optional. If +immediate+ is true, the request will be made using
778
+ # openid.mode=checkid_immediate instead of the standard
779
+ # openid.mode=checkid_setup.
780
+ #
781
+ # ==Return Value
782
+ # Return a string which is the URL to which you should redirect the user.
783
+ def redirect_url(trust_root, return_to, immediate=false)
784
+ # add the nonce into the return_to url
785
+ return_to = OpenID::Util.append_args(return_to, @return_to_args)
786
+
787
+ redir_args = {
788
+ "openid.identity" => @server_id,
789
+ "openid.return_to" => return_to,
790
+ "openid.trust_root" => trust_root,
791
+ "openid.mode" => immediate ? 'checkid_immediate' : 'checkid_setup'
792
+ }
793
+
794
+ redir_args["openid.assoc_handle"] = @assoc.handle if @assoc
795
+ redir_args.update(@extra_args)
796
+
797
+ return OpenID::Util.append_args(server_url, redir_args).to_s
798
+ end
799
+
800
+ # get the return_to URL
801
+ def return_to(return_to)
802
+ OpenID::Util.append_args(return_to, @return_to_args)
803
+ end
804
+
805
+ # Add an openid extension argument to the request. A simple resitration
806
+ # request may look something like:
807
+ #
808
+ # req.add_extension_arg('sreg','required','email')
809
+ # req.add_extension_arg('sreg','optional','nickname,gender')
810
+ # req.add_extension_arg('sreg','policy_url','http://example.com/policy')
811
+ def add_extension_arg(namespace, key, value)
812
+ @extra_args['openid.'+namespace+'.'+key] = value
813
+ end
814
+
815
+ def add_arg(key, value)
816
+ @extra_args[key] = value
817
+ end
818
+
819
+ # Checks to see if the user's OpenID server additionally supports
820
+ # the extensions service type url provided.
821
+ def uses_extension?(extension_url)
822
+ return false unless extension_url
823
+
824
+ @service.service_types.each do |url|
825
+ if OpenID::Util.urls_equal?(url, extension_url)
826
+ return true
827
+ end
828
+ end
829
+
830
+ return false
831
+ end
832
+
833
+ end
834
+
835
+ # Encapsulates the information that is useful after a successful
836
+ # Consumer.complete call. Verified identity URL and
837
+ # signed extension response values are available through this object.
838
+ class SuccessResponse < OpenIDStatus
839
+
840
+ attr_reader :identity_url
841
+ attr_accessor :service
842
+
843
+ # Instances of this object will be created for you automatically
844
+ # by OpenID::Consumer. You should never have to construct this
845
+ # object yourself.
846
+ def initialize(identity_url, query)
847
+ super(SUCCESS)
848
+ @identity_url = identity_url
849
+ @query = query
850
+ @service = nil
851
+ end
852
+
853
+ # Returns all the arguments from an extension's namespace. For example
854
+ #
855
+ # response.extension_response('sreg')
856
+ #
857
+ # may return something like:
858
+ #
859
+ # {'email' => 'mayor@example.com', 'nickname' => 'MayorMcCheese'}
860
+ #
861
+ # The extension namespace is not included in the keys of the returned
862
+ # hash. Values returned from this method are guaranteed to be signed.
863
+ # Calling this method should be the *only* way you access extension
864
+ # response data!
865
+ def extension_response(extension_name)
866
+ prefix = extension_name
867
+
868
+ signed = @query['openid.signed']
869
+ return nil if signed.nil?
870
+
871
+ signed = signed.split(',')
872
+ extension_args = {}
873
+ extension_prefix = prefix + '.'
874
+
875
+ signed.each do |arg|
876
+ if arg.index(extension_prefix) == 0
877
+ query_key = 'openid.'+arg
878
+ extension_args[arg[(1+prefix.length..-1)]] = @query[query_key]
879
+ end
880
+ end
881
+
882
+ return extension_args
883
+ end
884
+
885
+ end
886
+
887
+ # Object returned from Consumer.complete when the auth request failed.
888
+ # The +identity_url+, +msg+, and +service+ methods may contain useful
889
+ # information about the failure if it is available. These methods will
890
+ # return nil if no useful info can be determined.
891
+ class FailureResponse < OpenIDStatus
892
+
893
+ attr_accessor :identity_url, :msg, :service
894
+
895
+ def initialize(identity_url=nil, msg=nil)
896
+ super(FAILURE)
897
+ @identity_url = identity_url
898
+ @msg = msg
899
+ end
900
+
901
+ end
902
+
903
+ # Returned by Consumer.begin in immediate mode when the user needs to
904
+ # log into the OpenID server. User should be redirected to the value
905
+ # returned from the +setup_url+ method.
906
+ class SetupNeededResponse < OpenIDStatus
907
+
908
+ attr_reader :setup_url
909
+ attr_accessor :identity_url, :service
910
+
911
+ def initialize(setup_url)
912
+ super(SETUP_NEEDED)
913
+ @setup_url = setup_url
914
+ end
915
+
916
+ end
917
+
918
+ # Response returned from Consumer.complete when the user cancels the
919
+ # OpenID transaction.
920
+ class CancelResponse < OpenIDStatus
921
+ attr_accessor :identity_url, :service
922
+ def initialize(identity_url)
923
+ super(CANCEL)
924
+ @identity_url = identity_url
925
+ end
926
+ end
927
+
928
+ end