ruby-openid 1.0

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

Potentially problematic release.


This version of ruby-openid might be problematic. Click here for more details.

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