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.
- 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
|