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.
- data/COPYING +21 -0
- data/INSTALL +34 -0
- data/README +67 -0
- data/TODO +9 -0
- data/examples/README +54 -0
- data/examples/cacert.pem +7815 -0
- data/examples/consumer.rb +285 -0
- data/examples/openid-store/associations/http-localhost_3A3000_2Fserver-EMQbAy3NnHVzA.s0u5KAcplKGzo +6 -0
- data/examples/openid-store/auth_key +1 -0
- data/examples/rails_active_record_store/README +59 -0
- data/examples/rails_active_record_store/XX_add_openidstore.rb +30 -0
- data/examples/rails_active_record_store/models/openid_association.rb +12 -0
- data/examples/rails_active_record_store/models/openid_nonce.rb +3 -0
- data/examples/rails_active_record_store/models/openid_setting.rb +2 -0
- data/examples/rails_active_record_store/openid_helper.rb +91 -0
- data/examples/rails_active_record_store/openidstore_test.rb +15 -0
- data/examples/rails_active_record_store/schema.mysql.sql +22 -0
- data/examples/rails_active_record_store/schema.postgresql.sql +21 -0
- data/examples/rails_active_record_store/schema.sqlite.sql +21 -0
- data/examples/rails_openid_login_generator/USAGE +23 -0
- data/examples/rails_openid_login_generator/openid_login_generator.rb +36 -0
- data/examples/rails_openid_login_generator/templates/README +116 -0
- data/examples/rails_openid_login_generator/templates/controller.rb +116 -0
- data/examples/rails_openid_login_generator/templates/controller_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/helper.rb +2 -0
- data/examples/rails_openid_login_generator/templates/openid_login_system.rb +87 -0
- data/examples/rails_openid_login_generator/templates/user.rb +14 -0
- data/examples/rails_openid_login_generator/templates/user_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/users.yml +0 -0
- data/examples/rails_openid_login_generator/templates/view_login.rhtml +15 -0
- data/examples/rails_openid_login_generator/templates/view_logout.rhtml +10 -0
- data/examples/rails_openid_login_generator/templates/view_welcome.rhtml +9 -0
- data/examples/rails_server/README +153 -0
- data/examples/rails_server/Rakefile +10 -0
- data/examples/rails_server/app/controllers/application.rb +4 -0
- data/examples/rails_server/app/controllers/login_controller.rb +35 -0
- data/examples/rails_server/app/controllers/server_controller.rb +185 -0
- data/examples/rails_server/app/helpers/application_helper.rb +3 -0
- data/examples/rails_server/app/helpers/login_helper.rb +2 -0
- data/examples/rails_server/app/helpers/server_helper.rb +9 -0
- data/examples/rails_server/app/views/layouts/server.rhtml +61 -0
- data/examples/rails_server/app/views/login/index.rhtml +32 -0
- data/examples/rails_server/app/views/server/decide.rhtml +11 -0
- data/examples/rails_server/config/boot.rb +19 -0
- data/examples/rails_server/config/database.yml +85 -0
- data/examples/rails_server/config/environment.rb +53 -0
- data/examples/rails_server/config/environments/development.rb +19 -0
- data/examples/rails_server/config/environments/production.rb +19 -0
- data/examples/rails_server/config/environments/test.rb +19 -0
- data/examples/rails_server/config/routes.rb +23 -0
- data/examples/rails_server/db/openid-store/associations/http-localhost_2F_7Cnormal-YU.tkND1J4fEZhnuAoT5Zc0yCA0 +6 -0
- data/examples/rails_server/doc/README_FOR_APP +2 -0
- data/examples/rails_server/log/development.log +6059 -0
- data/examples/rails_server/log/production.log +0 -0
- data/examples/rails_server/log/server.log +0 -0
- data/examples/rails_server/log/test.log +0 -0
- data/examples/rails_server/public/404.html +8 -0
- data/examples/rails_server/public/500.html +8 -0
- data/examples/rails_server/public/dispatch.cgi +12 -0
- data/examples/rails_server/public/dispatch.fcgi +26 -0
- data/examples/rails_server/public/dispatch.rb +12 -0
- data/examples/rails_server/public/favicon.ico +0 -0
- data/examples/rails_server/public/images/rails.png +0 -0
- data/examples/rails_server/public/javascripts/controls.js +750 -0
- data/examples/rails_server/public/javascripts/dragdrop.js +584 -0
- data/examples/rails_server/public/javascripts/effects.js +854 -0
- data/examples/rails_server/public/javascripts/prototype.js +1785 -0
- data/examples/rails_server/public/robots.txt +1 -0
- data/examples/rails_server/script/about +3 -0
- data/examples/rails_server/script/breakpointer +3 -0
- data/examples/rails_server/script/console +3 -0
- data/examples/rails_server/script/destroy +3 -0
- data/examples/rails_server/script/generate +3 -0
- data/examples/rails_server/script/performance/benchmarker +3 -0
- data/examples/rails_server/script/performance/profiler +3 -0
- data/examples/rails_server/script/plugin +3 -0
- data/examples/rails_server/script/process/reaper +3 -0
- data/examples/rails_server/script/process/spawner +3 -0
- data/examples/rails_server/script/process/spinner +3 -0
- data/examples/rails_server/script/runner +3 -0
- data/examples/rails_server/script/server +3 -0
- data/examples/rails_server/test/functional/login_controller_test.rb +18 -0
- data/examples/rails_server/test/functional/server_controller_test.rb +18 -0
- data/examples/rails_server/test/test_helper.rb +28 -0
- data/lib/hmac-md5.rb +11 -0
- data/lib/hmac-rmd160.rb +11 -0
- data/lib/hmac-sha1.rb +11 -0
- data/lib/hmac-sha2.rb +25 -0
- data/lib/hmac.rb +112 -0
- data/lib/openid/association.rb +109 -0
- data/lib/openid/consumer.rb +928 -0
- data/lib/openid/dh.rb +48 -0
- data/lib/openid/discovery.rb +89 -0
- data/lib/openid/fetchers.rb +119 -0
- data/lib/openid/filestore.rb +315 -0
- data/lib/openid/htmltokenizer.rb +355 -0
- data/lib/openid/parse.rb +23 -0
- data/lib/openid/server.rb +951 -0
- data/lib/openid/service.rb +135 -0
- data/lib/openid/stores.rb +178 -0
- data/lib/openid/trustroot.rb +100 -0
- data/lib/openid/util.rb +273 -0
- data/test/assoc.rb +38 -0
- data/test/consumer.rb +384 -0
- data/test/dh.rb +20 -0
- data/test/extensions.rb +30 -0
- data/test/linkparse.rb +305 -0
- data/test/runtests.rb +11 -0
- data/test/server2.rb +1053 -0
- data/test/storetestcase.rb +172 -0
- data/test/teststore.rb +23 -0
- data/test/trustroot.rb +113 -0
- data/test/util.rb +56 -0
- 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,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
data/lib/hmac-rmd160.rb
ADDED
data/lib/hmac-sha1.rb
ADDED
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
|