ruby-openid 1.1.4 → 2.0.1
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/INSTALL +0 -9
- data/README +21 -22
- data/UPGRADE +117 -0
- data/admin/runtests.rb +36 -0
- data/examples/README +13 -21
- data/examples/active_record_openid_store/README +8 -3
- data/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +4 -8
- data/examples/active_record_openid_store/XXX_upgrade_open_id_store.rb +26 -0
- data/examples/active_record_openid_store/lib/association.rb +2 -0
- data/examples/active_record_openid_store/lib/openid_ar_store.rb +22 -47
- data/examples/active_record_openid_store/test/store_test.rb +78 -48
- data/examples/discover +46 -0
- data/examples/{rails_server → rails_openid}/README +0 -0
- data/examples/{rails_server → rails_openid}/Rakefile +0 -0
- data/examples/{rails_server → rails_openid}/app/controllers/application.rb +0 -0
- data/examples/rails_openid/app/controllers/consumer_controller.rb +115 -0
- data/examples/{rails_server → rails_openid}/app/controllers/login_controller.rb +10 -2
- data/examples/rails_openid/app/controllers/server_controller.rb +265 -0
- data/examples/{rails_server → rails_openid}/app/helpers/application_helper.rb +0 -0
- data/examples/{rails_server → rails_openid}/app/helpers/login_helper.rb +0 -0
- data/examples/{rails_server → rails_openid}/app/helpers/server_helper.rb +0 -0
- data/examples/rails_openid/app/views/consumer/index.rhtml +81 -0
- data/examples/rails_openid/app/views/consumer/start.rhtml +8 -0
- data/examples/{rails_server → rails_openid}/app/views/layouts/server.rhtml +0 -0
- data/examples/{rails_server → rails_openid}/app/views/login/index.rhtml +1 -1
- data/examples/rails_openid/app/views/server/decide.rhtml +26 -0
- data/examples/{rails_server → rails_openid}/config/boot.rb +0 -0
- data/examples/{rails_server → rails_openid}/config/database.yml +0 -0
- data/examples/{rails_server → rails_openid}/config/environment.rb +0 -0
- data/examples/{rails_server → rails_openid}/config/environments/development.rb +0 -0
- data/examples/{rails_server → rails_openid}/config/environments/production.rb +0 -0
- data/examples/{rails_server → rails_openid}/config/environments/test.rb +0 -0
- data/examples/{rails_server → rails_openid}/config/routes.rb +2 -1
- data/examples/{rails_server → rails_openid}/doc/README_FOR_APP +0 -0
- data/examples/{rails_server → rails_openid}/public/404.html +0 -0
- data/examples/{rails_server → rails_openid}/public/500.html +0 -0
- data/examples/{rails_server → rails_openid}/public/dispatch.cgi +0 -0
- data/examples/{rails_server → rails_openid}/public/dispatch.fcgi +0 -0
- data/examples/{rails_server → rails_openid}/public/dispatch.rb +0 -0
- data/examples/{rails_server → rails_openid}/public/favicon.ico +0 -0
- data/examples/rails_openid/public/images/openid_login_bg.gif +0 -0
- data/examples/{rails_server → rails_openid}/public/javascripts/controls.js +0 -0
- data/examples/{rails_server → rails_openid}/public/javascripts/dragdrop.js +0 -0
- data/examples/{rails_server → rails_openid}/public/javascripts/effects.js +0 -0
- data/examples/{rails_server → rails_openid}/public/javascripts/prototype.js +0 -0
- data/examples/{rails_server → rails_openid}/public/robots.txt +0 -0
- data/examples/{rails_server → rails_openid}/script/about +0 -0
- data/examples/{rails_server → rails_openid}/script/breakpointer +0 -0
- data/examples/{rails_server → rails_openid}/script/console +0 -0
- data/examples/{rails_server → rails_openid}/script/destroy +0 -0
- data/examples/{rails_server → rails_openid}/script/generate +0 -0
- data/examples/{rails_server → rails_openid}/script/performance/benchmarker +0 -0
- data/examples/{rails_server → rails_openid}/script/performance/profiler +0 -0
- data/examples/{rails_server → rails_openid}/script/plugin +0 -0
- data/examples/{rails_server → rails_openid}/script/process/reaper +0 -0
- data/examples/{rails_server → rails_openid}/script/process/spawner +0 -0
- data/examples/{rails_server → rails_openid}/script/process/spinner +0 -0
- data/examples/{rails_server → rails_openid}/script/runner +0 -0
- data/examples/{rails_server → rails_openid}/script/server +0 -0
- data/examples/{rails_server → rails_openid}/test/functional/login_controller_test.rb +0 -0
- data/examples/{rails_server → rails_openid}/test/functional/server_controller_test.rb +0 -0
- data/examples/{rails_server → rails_openid}/test/test_helper.rb +0 -0
- data/lib/{hmac.rb → hmac/hmac.rb} +0 -0
- data/lib/{hmac-sha1.rb → hmac/sha1.rb} +1 -1
- data/lib/{hmac-sha2.rb → hmac/sha2.rb} +1 -1
- data/lib/openid/association.rb +213 -73
- data/lib/openid/consumer/associationmanager.rb +338 -0
- data/lib/openid/consumer/checkid_request.rb +175 -0
- data/lib/openid/consumer/discovery.rb +480 -0
- data/lib/openid/consumer/discovery_manager.rb +123 -0
- data/lib/openid/consumer/html_parse.rb +136 -0
- data/lib/openid/consumer/idres.rb +525 -0
- data/lib/openid/consumer/responses.rb +133 -0
- data/lib/openid/consumer.rb +280 -807
- data/lib/openid/cryptutil.rb +85 -0
- data/lib/openid/dh.rb +60 -23
- data/lib/openid/extension.rb +31 -0
- data/lib/openid/extensions/ax.rb +506 -0
- data/lib/openid/extensions/pape.rb +182 -0
- data/lib/openid/extensions/sreg.rb +275 -0
- data/lib/openid/extras.rb +11 -0
- data/lib/openid/fetchers.rb +132 -93
- data/lib/openid/kvform.rb +133 -0
- data/lib/openid/kvpost.rb +56 -0
- data/lib/openid/message.rb +534 -0
- data/lib/openid/protocolerror.rb +6 -0
- data/lib/openid/server.rb +1215 -666
- data/lib/openid/store/filesystem.rb +271 -0
- data/lib/openid/store/interface.rb +75 -0
- data/lib/openid/store/memory.rb +84 -0
- data/lib/openid/store/nonce.rb +68 -0
- data/lib/openid/trustroot.rb +314 -87
- data/lib/openid/urinorm.rb +37 -34
- data/lib/openid/util.rb +42 -220
- data/lib/openid/yadis/accept.rb +148 -0
- data/lib/openid/yadis/constants.rb +21 -0
- data/lib/openid/yadis/discovery.rb +153 -0
- data/lib/openid/yadis/filters.rb +205 -0
- data/lib/openid/{htmltokenizer.rb → yadis/htmltokenizer.rb} +1 -54
- data/lib/openid/yadis/parsehtml.rb +36 -0
- data/lib/openid/yadis/services.rb +42 -0
- data/lib/openid/yadis/xrds.rb +171 -0
- data/lib/openid/yadis/xri.rb +90 -0
- data/lib/openid/yadis/xrires.rb +106 -0
- data/lib/openid.rb +1 -4
- data/test/data/accept.txt +124 -0
- data/test/data/dh.txt +29 -0
- data/test/data/example-xrds.xml +14 -0
- data/test/data/linkparse.txt +587 -0
- data/test/data/n2b64 +650 -0
- data/test/data/test1-discover.txt +137 -0
- data/test/data/test1-parsehtml.txt +128 -0
- data/test/data/test_discover/openid.html +11 -0
- data/test/data/test_discover/openid2.html +11 -0
- data/test/data/test_discover/openid2_xrds.xml +12 -0
- data/test/data/test_discover/openid2_xrds_no_local_id.xml +11 -0
- data/test/data/test_discover/openid_1_and_2.html +11 -0
- data/test/data/test_discover/openid_1_and_2_xrds.xml +16 -0
- data/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml +17 -0
- data/test/data/test_discover/openid_and_yadis.html +12 -0
- data/test/data/test_discover/openid_no_delegate.html +10 -0
- data/test/data/test_discover/yadis_0entries.xml +12 -0
- data/test/data/test_discover/yadis_2_bad_local_id.xml +15 -0
- data/test/data/test_discover/yadis_2entries_delegate.xml +22 -0
- data/test/data/test_discover/yadis_2entries_idp.xml +21 -0
- data/test/data/test_discover/yadis_another_delegate.xml +14 -0
- data/test/data/test_discover/yadis_idp.xml +12 -0
- data/test/data/test_discover/yadis_idp_delegate.xml +13 -0
- data/test/data/test_discover/yadis_no_delegate.xml +11 -0
- data/test/data/test_xrds/=j3h.2007.11.14.xrds +25 -0
- data/test/data/test_xrds/README +12 -0
- data/test/data/test_xrds/delegated-20060809-r1.xrds +34 -0
- data/test/data/test_xrds/delegated-20060809-r2.xrds +34 -0
- data/test/data/test_xrds/delegated-20060809.xrds +34 -0
- data/test/data/test_xrds/no-xrd.xml +7 -0
- data/test/data/test_xrds/not-xrds.xml +2 -0
- data/test/data/test_xrds/prefixsometimes.xrds +34 -0
- data/test/data/test_xrds/ref.xrds +109 -0
- data/test/data/test_xrds/sometimesprefix.xrds +34 -0
- data/test/data/test_xrds/spoof1.xrds +25 -0
- data/test/data/test_xrds/spoof2.xrds +25 -0
- data/test/data/test_xrds/spoof3.xrds +37 -0
- data/test/data/test_xrds/status222.xrds +9 -0
- data/test/data/test_xrds/valid-populated-xrds.xml +39 -0
- data/test/data/trustroot.txt +147 -0
- data/test/discoverdata.rb +131 -0
- data/test/test_accept.rb +170 -0
- data/test/test_association.rb +266 -0
- data/test/test_associationmanager.rb +899 -0
- data/test/test_ax.rb +587 -0
- data/test/test_checkid_request.rb +297 -0
- data/test/test_consumer.rb +257 -0
- data/test/test_cryptutil.rb +117 -0
- data/test/test_dh.rb +86 -0
- data/test/test_discover.rb +772 -0
- data/test/test_discovery_manager.rb +262 -0
- data/test/test_extras.rb +35 -0
- data/test/test_fetchers.rb +472 -0
- data/test/test_filters.rb +270 -0
- data/test/test_idres.rb +816 -0
- data/test/test_kvform.rb +165 -0
- data/test/test_kvpost.rb +65 -0
- data/test/test_linkparse.rb +101 -0
- data/test/test_message.rb +1058 -0
- data/test/test_nonce.rb +89 -0
- data/test/test_openid_yadis.rb +178 -0
- data/test/test_pape.rb +233 -0
- data/test/test_parsehtml.rb +80 -0
- data/test/test_responses.rb +63 -0
- data/test/test_server.rb +2270 -0
- data/test/test_sreg.rb +479 -0
- data/test/test_stores.rb +269 -0
- data/test/test_trustroot.rb +112 -0
- data/test/{urinorm.rb → test_urinorm.rb} +6 -3
- data/test/test_util.rb +144 -0
- data/test/test_xrds.rb +160 -0
- data/test/test_xri.rb +48 -0
- data/test/test_xrires.rb +63 -0
- data/test/test_yadis_discovery.rb +207 -0
- data/test/testutil.rb +116 -0
- data/test/util.rb +47 -50
- metadata +233 -143
- data/examples/consumer.rb +0 -290
- data/examples/rails_openid_login_generator/openid_login_generator-0.1.gem +0 -0
- data/examples/rails_server/app/controllers/server_controller.rb +0 -190
- data/examples/rails_server/app/views/server/decide.rhtml +0 -11
- data/examples/rails_server/public/images/rails.png +0 -0
- data/lib/hmac-md5.rb +0 -11
- data/lib/hmac-rmd160.rb +0 -11
- data/lib/openid/discovery.rb +0 -122
- data/lib/openid/filestore.rb +0 -315
- data/lib/openid/parse.rb +0 -23
- data/lib/openid/service.rb +0 -147
- data/lib/openid/stores.rb +0 -178
- data/test/assoc.rb +0 -38
- data/test/consumer.rb +0 -376
- data/test/data/brian.xrds +0 -16
- data/test/data/brianellin.mylid.xrds +0 -42
- data/test/dh.rb +0 -20
- data/test/extensions.rb +0 -30
- data/test/linkparse.rb +0 -305
- data/test/runtests.rb +0 -22
- data/test/server2.rb +0 -1053
- data/test/service.rb +0 -47
- data/test/storetestcase.rb +0 -172
- data/test/teststore.rb +0 -47
- data/test/trustroot.rb +0 -117
data/lib/openid/server.rb
CHANGED
|
@@ -1,960 +1,1509 @@
|
|
|
1
|
+
|
|
2
|
+
require 'openid/cryptutil'
|
|
1
3
|
require 'openid/util'
|
|
2
|
-
require 'openid/association'
|
|
3
4
|
require 'openid/dh'
|
|
5
|
+
require 'openid/store/nonce'
|
|
4
6
|
require 'openid/trustroot'
|
|
7
|
+
require 'openid/association'
|
|
8
|
+
require 'openid/message'
|
|
9
|
+
|
|
10
|
+
require 'time'
|
|
5
11
|
|
|
6
12
|
module OpenID
|
|
7
13
|
|
|
8
|
-
# This module contains classes specific to implemeting an OpenID
|
|
9
|
-
# server.
|
|
10
|
-
#
|
|
11
|
-
# ==Overview
|
|
12
|
-
#
|
|
13
|
-
# An OpenID server must perform three tasks:
|
|
14
|
-
#
|
|
15
|
-
# 1. Examine the incoming request to determine its nature and validity.
|
|
16
|
-
#
|
|
17
|
-
# 2. Make a decision about how to respond to this request.
|
|
18
|
-
#
|
|
19
|
-
# 3. Format the response according to the protocol.
|
|
20
|
-
#
|
|
21
|
-
# The first and last of these tasks may performed by
|
|
22
|
-
# the Server.decode_request and Server.encode_response methods of the
|
|
23
|
-
# OpenID::Server::Server object. Who gets to do the intermediate task --
|
|
24
|
-
# deciding how to respond to the request -- will depend on what type of
|
|
25
|
-
# request it is.
|
|
26
|
-
#
|
|
27
|
-
# If it's a request to authenticate a user (a checkid_setup or
|
|
28
|
-
# checkid_immediate request), you need to decide if you will assert
|
|
29
|
-
# that this user may claim the identity in question. Exactly how you do
|
|
30
|
-
# that is a matter of application policy, but it generally involves making
|
|
31
|
-
# sure the user has an account with your system and is logged in, checking
|
|
32
|
-
# to see if that identity is hers to claim, and verifying with the user that
|
|
33
|
-
# she does consent to releasing that information to the party making the
|
|
34
|
-
# request. Do this by examining the properties of the CheckIDRequest
|
|
35
|
-
# object, and when you've come to a decision, form a response by calling
|
|
36
|
-
# CheckIDRequest.answer.
|
|
37
|
-
#
|
|
38
|
-
# Other types of requests relate to establishing associations between client
|
|
39
|
-
# and server and verifing the authenticity of previous communications.
|
|
40
|
-
# Server contains all the logic and data necessary to respond to
|
|
41
|
-
# such requests; just pass it to Server.handle_request.
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# ==OpenID Extensions
|
|
45
|
-
#
|
|
46
|
-
# Do you want to provide other information for your users
|
|
47
|
-
# in addition to authentication? Version 1.2 of the OpenID
|
|
48
|
-
# protocol allows consumers to add extensions to their requests.
|
|
49
|
-
# For example, with sites using the Simple Registration Extension
|
|
50
|
-
# http://www.openidenabled.com/openid/simple-registration-extension/,
|
|
51
|
-
# a user can agree to have their nickname and e-mail address sent to a
|
|
52
|
-
# site when they sign up.
|
|
53
|
-
#
|
|
54
|
-
# Since extensions do not change the way OpenID authentication works,
|
|
55
|
-
# code to handle extension requests may be completely separate from the
|
|
56
|
-
# OpenIDRequest class here. But you'll likely want data sent back by
|
|
57
|
-
# your extension to be signed. OpenIDResponse provides methods with
|
|
58
|
-
# which you can add data to it which can be signed with the other data in
|
|
59
|
-
# the OpenID signature.
|
|
60
|
-
#
|
|
61
|
-
# For example when request is a checkid_* request:
|
|
62
|
-
#
|
|
63
|
-
# response = request.answer(true)
|
|
64
|
-
# # this will add a signed 'openid.sreg.timezone' parameter to the response
|
|
65
|
-
# response.add_field('sreg', 'timezone', 'America/Los_Angeles')
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# ==Stores
|
|
69
|
-
#
|
|
70
|
-
# The OpenID server needs to maintain state between requests in order
|
|
71
|
-
# to function. Its mechanism for doing this is called a store. The
|
|
72
|
-
# store interface is defined in OpenID::Store.
|
|
73
|
-
# Additionally, several concrete store implementations are provided, so that
|
|
74
|
-
# most sites won't need to implement a custom store. For a store backed
|
|
75
|
-
# by flat files on disk, see OpenID::FilesystemStore.
|
|
76
|
-
#
|
|
77
|
-
# ==Upgrading
|
|
78
|
-
#
|
|
79
|
-
# The keys by which a server looks up associations in its store have changed
|
|
80
|
-
# in version 1.2 of this library. If your store has entries created from
|
|
81
|
-
# version 1.0 code, you should empty it.
|
|
82
14
|
module Server
|
|
83
|
-
|
|
84
|
-
HTTP_REDIRECT = 302
|
|
15
|
+
|
|
85
16
|
HTTP_OK = 200
|
|
17
|
+
HTTP_REDIRECT = 302
|
|
86
18
|
HTTP_ERROR = 400
|
|
87
|
-
|
|
19
|
+
|
|
88
20
|
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
|
|
89
|
-
OPENID_PREFIX = 'openid.'
|
|
90
21
|
|
|
91
22
|
ENCODE_KVFORM = ['kvform'].freeze
|
|
92
23
|
ENCODE_URL = ['URL/redirect'].freeze
|
|
24
|
+
ENCODE_HTML_FORM = ['HTML form'].freeze
|
|
25
|
+
|
|
26
|
+
UNUSED = nil
|
|
93
27
|
|
|
94
|
-
# Represents an incoming OpenID request.
|
|
95
28
|
class OpenIDRequest
|
|
96
|
-
|
|
97
|
-
attr_reader :mode
|
|
29
|
+
attr_accessor :namespace, :message, :mode
|
|
98
30
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
31
|
+
# I represent an incoming OpenID request.
|
|
32
|
+
#
|
|
33
|
+
# Attributes:
|
|
34
|
+
# mode:: The "openid.mode" of this request
|
|
35
|
+
def initialize
|
|
36
|
+
@mode = nil
|
|
103
37
|
end
|
|
104
|
-
|
|
105
38
|
end
|
|
106
39
|
|
|
107
40
|
# A request to verify the validity of a previous response.
|
|
41
|
+
#
|
|
42
|
+
# See OpenID Specs, Verifying Directly with the OpenID Provider
|
|
43
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures>
|
|
108
44
|
class CheckAuthRequest < OpenIDRequest
|
|
109
|
-
|
|
110
|
-
attr_accessor :assoc_handle, :sig, :signed, :invalidate_handle
|
|
111
45
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
46
|
+
# The association handle the response was signed with.
|
|
47
|
+
attr_accessor :assoc_handle
|
|
48
|
+
|
|
49
|
+
# The message with the signature which wants checking.
|
|
50
|
+
attr_accessor :signed
|
|
51
|
+
|
|
52
|
+
# An association handle the client is asking about the validity
|
|
53
|
+
# of. May be nil.
|
|
54
|
+
attr_accessor :invalidate_handle
|
|
55
|
+
|
|
56
|
+
attr_accessor :sig
|
|
57
|
+
|
|
58
|
+
# Construct me.
|
|
123
59
|
#
|
|
124
|
-
#
|
|
125
|
-
# An array of pairs of [key, value], where key is the signed paramter
|
|
126
|
-
# name without the "openid." prefix. Value is the String value of
|
|
127
|
-
# of the paramter.
|
|
60
|
+
# These parameters are assigned directly as class attributes.
|
|
128
61
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
62
|
+
# Parameters:
|
|
63
|
+
# assoc_handle:: the association handle for this request
|
|
64
|
+
# signed:: The signed message
|
|
65
|
+
# invalidate_handle:: An association handle that the relying
|
|
66
|
+
# party is checking to see if it is invalid
|
|
67
|
+
def initialize(assoc_handle, signed, invalidate_handle=nil)
|
|
68
|
+
super()
|
|
69
|
+
|
|
70
|
+
@mode = "check_authentication"
|
|
71
|
+
@required_fields = ["identity", "return_to", "response_nonce"].freeze
|
|
72
|
+
|
|
73
|
+
@sig = nil
|
|
134
74
|
@assoc_handle = assoc_handle
|
|
135
|
-
@sig = sig
|
|
136
75
|
@signed = signed
|
|
137
76
|
@invalidate_handle = invalidate_handle
|
|
77
|
+
@namespace = OPENID2_NS
|
|
138
78
|
end
|
|
139
79
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
80
|
+
# Construct me from an OpenID::Message.
|
|
81
|
+
def self.from_message(message, op_endpoint=UNUSED)
|
|
82
|
+
assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
|
|
83
|
+
invalidate_handle = message.get_arg(OPENID_NS, 'invalidate_handle')
|
|
84
|
+
|
|
85
|
+
signed = message.copy()
|
|
86
|
+
# openid.mode is currently check_authentication because
|
|
87
|
+
# that's the mode of this request. But the signature
|
|
88
|
+
# was made on something with a different openid.mode.
|
|
89
|
+
# http://article.gmane.org/gmane.comp.web.openid.general/537
|
|
90
|
+
if signed.has_key?(OPENID_NS, "mode")
|
|
91
|
+
signed.set_arg(OPENID_NS, "mode", "id_res")
|
|
152
92
|
end
|
|
153
93
|
|
|
154
|
-
|
|
155
|
-
|
|
94
|
+
obj = self.new(assoc_handle, signed, invalidate_handle)
|
|
95
|
+
obj.message = message
|
|
96
|
+
obj.namespace = message.get_openid_namespace()
|
|
97
|
+
obj.sig = message.get_arg(OPENID_NS, 'sig')
|
|
156
98
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if value.nil?
|
|
163
|
-
raise ProtocolError.new(query, "Couldn't find signed field #{field}")
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
signed_pairs << [field, value]
|
|
99
|
+
if !obj.assoc_handle or
|
|
100
|
+
!obj.sig
|
|
101
|
+
msg = sprintf("%s request missing required parameter from message %s",
|
|
102
|
+
obj.mode, message)
|
|
103
|
+
raise ProtocolError.new(message, msg)
|
|
167
104
|
end
|
|
168
|
-
|
|
169
|
-
return
|
|
105
|
+
|
|
106
|
+
return obj
|
|
170
107
|
end
|
|
171
108
|
|
|
172
|
-
# Respond to this request.
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
# and
|
|
109
|
+
# Respond to this request.
|
|
110
|
+
#
|
|
111
|
+
# Given a Signatory, I can check the validity of the signature
|
|
112
|
+
# and the invalidate_handle. I return a response with an
|
|
113
|
+
# is_valid (and, if appropriate invalidate_handle) field.
|
|
177
114
|
def answer(signatory)
|
|
178
|
-
is_valid = signatory.verify(@assoc_handle, @
|
|
179
|
-
|
|
180
|
-
|
|
115
|
+
is_valid = signatory.verify(@assoc_handle, @signed)
|
|
116
|
+
# Now invalidate that assoc_handle so it this checkAuth
|
|
117
|
+
# message cannot be replayed.
|
|
118
|
+
signatory.invalidate(@assoc_handle, dumb=true)
|
|
181
119
|
response = OpenIDResponse.new(self)
|
|
182
|
-
|
|
183
|
-
|
|
120
|
+
valid_str = is_valid ? "true" : "false"
|
|
121
|
+
response.fields.set_arg(OPENID_NS, 'is_valid', valid_str)
|
|
122
|
+
|
|
184
123
|
if @invalidate_handle
|
|
185
124
|
assoc = signatory.get_association(@invalidate_handle, false)
|
|
186
|
-
|
|
187
|
-
response.fields
|
|
188
|
-
|
|
125
|
+
if !assoc
|
|
126
|
+
response.fields.set_arg(
|
|
127
|
+
OPENID_NS, 'invalidate_handle', @invalidate_handle)
|
|
128
|
+
end
|
|
189
129
|
end
|
|
190
130
|
|
|
191
131
|
return response
|
|
192
132
|
end
|
|
193
133
|
|
|
134
|
+
def to_s
|
|
135
|
+
ih = nil
|
|
136
|
+
|
|
137
|
+
if @invalidate_handle
|
|
138
|
+
ih = sprintf(" invalidate? %s", @invalidate_handle)
|
|
139
|
+
else
|
|
140
|
+
ih = ""
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
s = sprintf("<%s handle: %s sig: %s: signed: %s%s>",
|
|
144
|
+
self.class, @assoc_handle,
|
|
145
|
+
@sig, @signed, ih)
|
|
146
|
+
return s
|
|
147
|
+
end
|
|
194
148
|
end
|
|
195
|
-
|
|
196
|
-
# An object that knows how to handle association requests with no
|
|
197
|
-
# session type.
|
|
198
|
-
class PlainTextServerSession
|
|
199
149
|
|
|
150
|
+
class BaseServerSession
|
|
151
|
+
attr_reader :session_type
|
|
152
|
+
|
|
153
|
+
def initialize(session_type, allowed_assoc_types)
|
|
154
|
+
@session_type = session_type
|
|
155
|
+
@allowed_assoc_types = allowed_assoc_types.dup.freeze
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def allowed_assoc_type?(typ)
|
|
159
|
+
@allowed_assoc_types.member?(typ)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# An object that knows how to handle association requests with
|
|
164
|
+
# no session type.
|
|
165
|
+
#
|
|
166
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
|
167
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
168
|
+
class PlainTextServerSession < BaseServerSession
|
|
169
|
+
# The session_type for this association session. There is no
|
|
170
|
+
# type defined for plain-text in the OpenID specification, so we
|
|
171
|
+
# use 'no-encryption'.
|
|
200
172
|
attr_reader :session_type
|
|
201
173
|
|
|
202
174
|
def initialize
|
|
203
|
-
|
|
175
|
+
super('no-encryption', ['HMAC-SHA1', 'HMAC-SHA256'])
|
|
204
176
|
end
|
|
205
177
|
|
|
206
|
-
def
|
|
207
|
-
new
|
|
178
|
+
def self.from_message(unused_request)
|
|
179
|
+
return self.new
|
|
208
180
|
end
|
|
209
|
-
|
|
181
|
+
|
|
210
182
|
def answer(secret)
|
|
211
|
-
return {'mac_key' =>
|
|
183
|
+
return {'mac_key' => Util.to_base64(secret)}
|
|
212
184
|
end
|
|
213
|
-
|
|
214
185
|
end
|
|
215
186
|
|
|
216
|
-
# An object that knows how to handle
|
|
217
|
-
#
|
|
218
|
-
|
|
187
|
+
# An object that knows how to handle association requests with the
|
|
188
|
+
# Diffie-Hellman session type.
|
|
189
|
+
#
|
|
190
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
|
191
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
192
|
+
class DiffieHellmanSHA1ServerSession < BaseServerSession
|
|
193
|
+
|
|
194
|
+
# The Diffie-Hellman algorithm values for this request
|
|
195
|
+
attr_accessor :dh
|
|
196
|
+
|
|
197
|
+
# The public key sent by the consumer in the associate request
|
|
198
|
+
attr_accessor :consumer_pubkey
|
|
199
|
+
|
|
200
|
+
# The session_type for this association session.
|
|
201
|
+
attr_reader :session_type
|
|
219
202
|
|
|
220
|
-
attr_reader :session_type, :dh, :consumer_pubkey
|
|
221
|
-
|
|
222
|
-
# In general you should create instances of DiffieHellmanServerSession
|
|
223
|
-
# with the from_query class method.
|
|
224
|
-
#
|
|
225
|
-
# ==Parameters
|
|
226
|
-
#
|
|
227
|
-
# [+dh+]
|
|
228
|
-
# OpenID::DiffieHellman instance
|
|
229
|
-
#
|
|
230
|
-
# [+consumer_pubkey+]
|
|
231
|
-
# Decoded "openid.dh_consumer_public".
|
|
232
203
|
def initialize(dh, consumer_pubkey)
|
|
204
|
+
super('DH-SHA1', ['HMAC-SHA1'])
|
|
205
|
+
|
|
206
|
+
@hash_func = CryptUtil.method('sha1')
|
|
233
207
|
@dh = dh
|
|
234
208
|
@consumer_pubkey = consumer_pubkey
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Construct me from OpenID Message
|
|
212
|
+
#
|
|
213
|
+
# Raises ProtocolError when parameters required to establish the
|
|
214
|
+
# session are missing.
|
|
215
|
+
def self.from_message(message)
|
|
216
|
+
dh_modulus = message.get_arg(OPENID_NS, 'dh_modulus')
|
|
217
|
+
dh_gen = message.get_arg(OPENID_NS, 'dh_gen')
|
|
218
|
+
if ((!dh_modulus and dh_gen) or
|
|
219
|
+
(!dh_gen and dh_modulus))
|
|
220
|
+
|
|
221
|
+
if !dh_modulus
|
|
222
|
+
missing = 'modulus'
|
|
223
|
+
else
|
|
224
|
+
missing = 'generator'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
raise ProtocolError.new(message,
|
|
228
|
+
sprintf('If non-default modulus or generator is ' +
|
|
229
|
+
'supplied, both must be supplied. Missing %s',
|
|
230
|
+
missing))
|
|
231
|
+
end
|
|
237
232
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
233
|
+
if dh_modulus or dh_gen
|
|
234
|
+
dh_modulus = CryptUtil.base64_to_num(dh_modulus)
|
|
235
|
+
dh_gen = CryptUtil.base64_to_num(dh_gen)
|
|
236
|
+
dh = DiffieHellman.new(dh_modulus, dh_gen)
|
|
237
|
+
else
|
|
238
|
+
dh = DiffieHellman.from_defaults()
|
|
239
|
+
end
|
|
245
240
|
|
|
246
|
-
consumer_pubkey =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
consumer_pubkey = message.get_arg(OPENID_NS, 'dh_consumer_public')
|
|
242
|
+
if !consumer_pubkey
|
|
243
|
+
raise ProtocolError.new(message,
|
|
244
|
+
sprintf("Public key for DH-SHA1 session " +
|
|
245
|
+
"not found in message %s", message))
|
|
250
246
|
end
|
|
251
247
|
|
|
252
|
-
consumer_pubkey =
|
|
253
|
-
|
|
248
|
+
consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey)
|
|
249
|
+
|
|
250
|
+
return self.new(dh, consumer_pubkey)
|
|
254
251
|
end
|
|
255
252
|
|
|
256
|
-
# Generate the arguments to be added to the response using +secret+.
|
|
257
253
|
def answer(secret)
|
|
258
|
-
mac_key = @dh.
|
|
254
|
+
mac_key = @dh.xor_secret(@hash_func,
|
|
255
|
+
@consumer_pubkey,
|
|
256
|
+
secret)
|
|
259
257
|
return {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
'dh_server_public' => CryptUtil.num_to_base64(@dh.public),
|
|
259
|
+
'enc_mac_key' => Util.to_base64(mac_key),
|
|
260
|
+
}
|
|
263
261
|
end
|
|
262
|
+
end
|
|
264
263
|
|
|
264
|
+
class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession
|
|
265
|
+
def initialize(*args)
|
|
266
|
+
super(*args)
|
|
267
|
+
@session_type = 'DH-SHA256'
|
|
268
|
+
@hash_func = CryptUtil.method('sha256')
|
|
269
|
+
@allowed_assoc_types = ['HMAC-SHA256'].freeze
|
|
270
|
+
end
|
|
265
271
|
end
|
|
266
272
|
|
|
267
|
-
# A request to establish an
|
|
268
|
-
#
|
|
273
|
+
# A request to establish an association.
|
|
274
|
+
#
|
|
275
|
+
# See OpenID Specs, Section 8: Establishing Associations
|
|
276
|
+
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
269
277
|
class AssociateRequest < OpenIDRequest
|
|
278
|
+
# An object that knows how to handle association requests of a
|
|
279
|
+
# certain type.
|
|
280
|
+
attr_accessor :session
|
|
281
|
+
|
|
282
|
+
# The type of association. Supported values include HMAC-SHA256
|
|
283
|
+
# and HMAC-SHA1
|
|
284
|
+
attr_accessor :assoc_type
|
|
270
285
|
|
|
271
|
-
|
|
286
|
+
@@session_classes = {
|
|
287
|
+
'no-encryption' => PlainTextServerSession,
|
|
288
|
+
'DH-SHA1' => DiffieHellmanSHA1ServerSession,
|
|
289
|
+
'DH-SHA256' => DiffieHellmanSHA256ServerSession,
|
|
290
|
+
}
|
|
272
291
|
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
# Construct me.
|
|
293
|
+
#
|
|
294
|
+
# The session is assigned directly as a class attribute. See my
|
|
295
|
+
# class documentation for its description.
|
|
296
|
+
def initialize(session, assoc_type)
|
|
297
|
+
super()
|
|
278
298
|
@session = session
|
|
299
|
+
@assoc_type = assoc_type
|
|
300
|
+
@namespace = OPENID2_NS
|
|
301
|
+
|
|
302
|
+
@mode = "associate"
|
|
279
303
|
end
|
|
280
304
|
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
305
|
+
# Construct me from an OpenID Message.
|
|
306
|
+
def self.from_message(message, op_endpoint=UNUSED)
|
|
307
|
+
if message.is_openid1()
|
|
308
|
+
session_type = message.get_arg(OPENID1_NS, 'session_type')
|
|
309
|
+
if session_type == 'no-encryption'
|
|
310
|
+
Util.log('Received OpenID 1 request with a no-encryption ' +
|
|
311
|
+
'assocaition session type. Continuing anyway.')
|
|
312
|
+
elsif !session_type
|
|
313
|
+
session_type = 'no-encryption'
|
|
314
|
+
end
|
|
289
315
|
else
|
|
290
|
-
|
|
291
|
-
|
|
316
|
+
session_type = message.get_arg(OPENID2_NS, 'session_type')
|
|
317
|
+
if !session_type
|
|
318
|
+
raise ProtocolError.new(message,
|
|
319
|
+
text="session_type missing from request")
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
session_class = @@session_classes[session_type]
|
|
324
|
+
|
|
325
|
+
if !session_class
|
|
326
|
+
raise ProtocolError.new(message,
|
|
327
|
+
sprintf("Unknown session type %s", session_type))
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
begin
|
|
331
|
+
session = session_class.from_message(message)
|
|
332
|
+
rescue ArgumentError => why
|
|
333
|
+
# XXX
|
|
334
|
+
raise ProtocolError.new(message,
|
|
335
|
+
sprintf('Error parsing %s session: %s',
|
|
336
|
+
session_type, why))
|
|
292
337
|
end
|
|
293
|
-
|
|
294
|
-
|
|
338
|
+
|
|
339
|
+
assoc_type = message.get_arg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
|
|
340
|
+
if !session.allowed_assoc_type?(assoc_type)
|
|
341
|
+
msg = sprintf('Session type %s does not support association type %s',
|
|
342
|
+
session_type, assoc_type)
|
|
343
|
+
raise ProtocolError.new(message, msg)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
obj = self.new(session, assoc_type)
|
|
347
|
+
obj.message = message
|
|
348
|
+
obj.namespace = message.get_openid_namespace()
|
|
349
|
+
return obj
|
|
295
350
|
end
|
|
296
351
|
|
|
297
|
-
# Respond to this request with an association.
|
|
298
|
-
#
|
|
299
|
-
#
|
|
352
|
+
# Respond to this request with an association.
|
|
353
|
+
#
|
|
354
|
+
# assoc:: The association to send back.
|
|
355
|
+
#
|
|
356
|
+
# Returns a response with the association information, encrypted
|
|
357
|
+
# to the consumer's public key if appropriate.
|
|
300
358
|
def answer(assoc)
|
|
301
359
|
response = OpenIDResponse.new(self)
|
|
360
|
+
response.fields.update_args(OPENID_NS, {
|
|
361
|
+
'expires_in' => sprintf('%d', assoc.expires_in()),
|
|
362
|
+
'assoc_type' => @assoc_type,
|
|
363
|
+
'assoc_handle' => assoc.handle,
|
|
364
|
+
})
|
|
365
|
+
response.fields.update_args(OPENID_NS,
|
|
366
|
+
@session.answer(assoc.secret))
|
|
367
|
+
if @session.session_type != 'no-encryption'
|
|
368
|
+
response.fields.set_arg(
|
|
369
|
+
OPENID_NS, 'session_type', @session.session_type)
|
|
370
|
+
end
|
|
302
371
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
'assoc_type' => 'HMAC-SHA1',
|
|
306
|
-
'assoc_handle' => assoc.handle
|
|
307
|
-
}
|
|
372
|
+
return response
|
|
373
|
+
end
|
|
308
374
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if @
|
|
314
|
-
|
|
375
|
+
# Respond to this request indicating that the association type
|
|
376
|
+
# or association session type is not supported.
|
|
377
|
+
def answer_unsupported(message, preferred_association_type=nil,
|
|
378
|
+
preferred_session_type=nil)
|
|
379
|
+
if @message.is_openid1()
|
|
380
|
+
raise ProtocolError.new(@message)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
response = OpenIDResponse.new(self)
|
|
384
|
+
response.fields.set_arg(OPENID_NS, 'error_code', 'unsupported-type')
|
|
385
|
+
response.fields.set_arg(OPENID_NS, 'error', message)
|
|
386
|
+
|
|
387
|
+
if preferred_association_type
|
|
388
|
+
response.fields.set_arg(
|
|
389
|
+
OPENID_NS, 'assoc_type', preferred_association_type)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if preferred_session_type
|
|
393
|
+
response.fields.set_arg(
|
|
394
|
+
OPENID_NS, 'session_type', preferred_session_type)
|
|
315
395
|
end
|
|
316
396
|
|
|
317
397
|
return response
|
|
318
398
|
end
|
|
319
|
-
|
|
320
399
|
end
|
|
321
400
|
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
401
|
+
# A request to confirm the identity of a user.
|
|
402
|
+
#
|
|
403
|
+
# This class handles requests for openid modes
|
|
404
|
+
# +checkid_immediate+ and +checkid_setup+ .
|
|
325
405
|
class CheckIDRequest < OpenIDRequest
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
406
|
+
|
|
407
|
+
# Provided in smart mode requests, a handle for a previously
|
|
408
|
+
# established association. nil for dumb mode requests.
|
|
409
|
+
attr_accessor :assoc_handle
|
|
410
|
+
|
|
411
|
+
# Is this an immediate-mode request?
|
|
412
|
+
attr_accessor :immediate
|
|
413
|
+
|
|
414
|
+
# The URL to send the user agent back to to reply to this
|
|
415
|
+
# request.
|
|
416
|
+
attr_accessor :return_to
|
|
417
|
+
|
|
418
|
+
# The OP-local identifier being checked.
|
|
419
|
+
attr_accessor :identity
|
|
420
|
+
|
|
421
|
+
# The claimed identifier. Not present in OpenID 1.x
|
|
422
|
+
# messages.
|
|
423
|
+
attr_accessor :claimed_id
|
|
424
|
+
|
|
425
|
+
# This URL identifies the party making the request, and the user
|
|
426
|
+
# will use that to make her decision about what answer she
|
|
427
|
+
# trusts them to have. Referred to as "realm" in OpenID 2.0.
|
|
428
|
+
attr_accessor :trust_root
|
|
429
|
+
|
|
430
|
+
# mode:: +checkid_immediate+ or +checkid_setup+
|
|
431
|
+
attr_accessor :mode
|
|
432
|
+
|
|
433
|
+
attr_accessor :return_to, :op_endpoint
|
|
434
|
+
|
|
435
|
+
# These parameters are assigned directly as attributes,
|
|
436
|
+
# see the #CheckIDRequest class documentation for their
|
|
437
|
+
# descriptions.
|
|
438
|
+
#
|
|
439
|
+
# Raises #MalformedReturnURL when the +return_to+ URL is not
|
|
440
|
+
# a URL.
|
|
441
|
+
def initialize(identity, return_to, op_endpoint, trust_root=nil,
|
|
442
|
+
immediate=false, assoc_handle=nil)
|
|
443
|
+
@namespace = OPENID2_NS
|
|
444
|
+
@assoc_handle = assoc_handle
|
|
340
445
|
@identity = identity
|
|
446
|
+
@claimed_id = identity
|
|
341
447
|
@return_to = return_to
|
|
342
|
-
@trust_root = trust_root
|
|
343
|
-
@
|
|
344
|
-
|
|
345
|
-
|
|
448
|
+
@trust_root = trust_root or return_to
|
|
449
|
+
@op_endpoint = op_endpoint
|
|
450
|
+
|
|
451
|
+
if immediate
|
|
452
|
+
@immediate = true
|
|
453
|
+
@mode = "checkid_immediate"
|
|
454
|
+
else
|
|
455
|
+
@immediate = false
|
|
456
|
+
@mode = "checkid_setup"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
if @return_to and
|
|
460
|
+
!TrustRoot::TrustRoot.parse(@return_to)
|
|
461
|
+
raise MalformedReturnURL.new(nil, @return_to)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
if !trust_root_valid()
|
|
465
|
+
raise UntrustedReturnURL.new(nil, @return_to, @trust_root)
|
|
466
|
+
end
|
|
346
467
|
end
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# Create a CheckIDRequest object from a web query. May raise a
|
|
350
|
-
# ProtocolError if request is a malformed checkid_* reuquest.
|
|
351
|
-
def CheckIDRequest.from_query(query)
|
|
352
|
-
mode = query['openid.mode']
|
|
353
468
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
469
|
+
# Construct me from an OpenID message.
|
|
470
|
+
#
|
|
471
|
+
# message:: An OpenID checkid_* request Message
|
|
472
|
+
#
|
|
473
|
+
# op_endpoint:: The endpoint URL of the server that this
|
|
474
|
+
# message was sent to.
|
|
475
|
+
#
|
|
476
|
+
# Raises:
|
|
477
|
+
# ProtocolError:: When not all required parameters are present
|
|
478
|
+
# in the message.
|
|
479
|
+
#
|
|
480
|
+
# MalformedReturnURL:: When the +return_to+ URL is not a URL.
|
|
481
|
+
#
|
|
482
|
+
# UntrustedReturnURL:: When the +return_to+ URL is
|
|
483
|
+
# outside the +trust_root+.
|
|
484
|
+
def self.from_message(message, op_endpoint)
|
|
485
|
+
obj = self.allocate
|
|
486
|
+
obj.message = message
|
|
487
|
+
obj.namespace = message.get_openid_namespace()
|
|
488
|
+
obj.op_endpoint = op_endpoint
|
|
489
|
+
mode = message.get_arg(OPENID_NS, 'mode')
|
|
490
|
+
if mode == "checkid_immediate"
|
|
491
|
+
obj.immediate = true
|
|
492
|
+
obj.mode = "checkid_immediate"
|
|
493
|
+
else
|
|
494
|
+
obj.immediate = false
|
|
495
|
+
obj.mode = "checkid_setup"
|
|
496
|
+
end
|
|
358
497
|
|
|
359
|
-
|
|
360
|
-
|
|
498
|
+
obj.return_to = message.get_arg(OPENID_NS, 'return_to')
|
|
499
|
+
if obj.namespace == OPENID1_NS and !obj.return_to
|
|
500
|
+
msg = sprintf("Missing required field 'return_to' from %s",
|
|
501
|
+
message)
|
|
502
|
+
raise ProtocolError.new(message, msg)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
obj.identity = message.get_arg(OPENID_NS, 'identity')
|
|
506
|
+
if obj.identity and message.is_openid2()
|
|
507
|
+
obj.claimed_id = message.get_arg(OPENID_NS, 'claimed_id')
|
|
508
|
+
if !obj.claimed_id
|
|
509
|
+
s = ("OpenID 2.0 message contained openid.identity but not " +
|
|
510
|
+
"claimed_id")
|
|
511
|
+
raise ProtocolError.new(message, s)
|
|
512
|
+
end
|
|
513
|
+
else
|
|
514
|
+
obj.claimed_id = nil
|
|
515
|
+
end
|
|
361
516
|
|
|
362
|
-
|
|
363
|
-
|
|
517
|
+
if !obj.identity and obj.namespace == OPENID1_NS
|
|
518
|
+
s = "OpenID 1 message did not contain openid.identity"
|
|
519
|
+
raise ProtocolError.new(message, s)
|
|
364
520
|
end
|
|
365
521
|
|
|
366
|
-
|
|
367
|
-
|
|
522
|
+
# There's a case for making self.trust_root be a TrustRoot
|
|
523
|
+
# here. But if TrustRoot isn't currently part of the "public"
|
|
524
|
+
# API, I'm not sure it's worth doing.
|
|
525
|
+
if obj.namespace == OPENID1_NS
|
|
526
|
+
obj.trust_root = message.get_arg(
|
|
527
|
+
OPENID_NS, 'trust_root', obj.return_to)
|
|
528
|
+
else
|
|
529
|
+
obj.trust_root = message.get_arg(
|
|
530
|
+
OPENID_NS, 'realm', obj.return_to)
|
|
531
|
+
|
|
532
|
+
if !obj.return_to and
|
|
533
|
+
!obj.trust_root
|
|
534
|
+
raise ProtocolError.new(message, "openid.realm required when " +
|
|
535
|
+
"openid.return_to absent")
|
|
536
|
+
end
|
|
368
537
|
end
|
|
369
|
-
|
|
370
|
-
assoc_handle =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
538
|
+
|
|
539
|
+
obj.assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
|
|
540
|
+
|
|
541
|
+
# Using TrustRoot.parse here is a bit misleading, as we're not
|
|
542
|
+
# parsing return_to as a trust root at all. However, valid
|
|
543
|
+
# URLs are valid trust roots, so we can use this to get an
|
|
544
|
+
# idea if it is a valid URL. Not all trust roots are valid
|
|
545
|
+
# return_to URLs, however (particularly ones with wildcards),
|
|
546
|
+
# so this is still a little sketchy.
|
|
547
|
+
if obj.return_to and \
|
|
548
|
+
!TrustRoot::TrustRoot.parse(obj.return_to)
|
|
549
|
+
raise MalformedReturnURL.new(message, obj.return_to)
|
|
377
550
|
end
|
|
378
551
|
|
|
379
|
-
|
|
552
|
+
# I first thought that checking to see if the return_to is
|
|
553
|
+
# within the trust_root is premature here, a
|
|
554
|
+
# logic-not-decoding thing. But it was argued that this is
|
|
555
|
+
# really part of data validation. A request with an invalid
|
|
556
|
+
# trust_root/return_to is broken regardless of application,
|
|
557
|
+
# right?
|
|
558
|
+
if !obj.trust_root_valid()
|
|
559
|
+
raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
return obj
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Is the identifier to be selected by the IDP?
|
|
566
|
+
def id_select
|
|
567
|
+
# So IDPs don't have to import the constant
|
|
568
|
+
return @identity == IDENTIFIER_SELECT
|
|
380
569
|
end
|
|
381
570
|
|
|
382
|
-
#
|
|
383
|
-
# is under the supplied trust_root.
|
|
571
|
+
# Is my return_to under my trust_root?
|
|
384
572
|
def trust_root_valid
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
573
|
+
if !@trust_root
|
|
574
|
+
return true
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
tr = TrustRoot::TrustRoot.parse(@trust_root)
|
|
578
|
+
if !tr
|
|
579
|
+
raise MalformedTrustRoot.new(nil, @trust_root)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
if @return_to
|
|
583
|
+
return tr.validate_url(@return_to)
|
|
584
|
+
else
|
|
585
|
+
return true
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Does the relying party publish the return_to URL for this
|
|
590
|
+
# response under the realm? It is up to the provider to set a
|
|
591
|
+
# policy for what kinds of realms should be allowed. This
|
|
592
|
+
# return_to URL verification reduces vulnerability to
|
|
593
|
+
# data-theft attacks based on open proxies,
|
|
594
|
+
# corss-site-scripting, or open redirectors.
|
|
595
|
+
#
|
|
596
|
+
# This check should only be performed after making sure that
|
|
597
|
+
# the return_to URL matches the realm.
|
|
598
|
+
#
|
|
599
|
+
# Raises DiscoveryFailure if the realm
|
|
600
|
+
# URL does not support Yadis discovery (and so does not
|
|
601
|
+
# support the verification process).
|
|
602
|
+
#
|
|
603
|
+
# Returns true if the realm publishes a document with the
|
|
604
|
+
# return_to URL listed
|
|
605
|
+
def return_to_verified
|
|
606
|
+
return TrustRoot.verify_return_to(@trust_root, @return_to)
|
|
389
607
|
end
|
|
390
608
|
|
|
391
|
-
#
|
|
609
|
+
# Respond to this request.
|
|
392
610
|
#
|
|
393
|
-
#
|
|
611
|
+
# allow:: Allow this user to claim this identity, and allow the
|
|
612
|
+
# consumer to have this information?
|
|
394
613
|
#
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
# supplied identity and let the consumer have the information. The
|
|
398
|
-
# value of allow should be follow the following algorithm:
|
|
614
|
+
# server_url:: DEPRECATED. Passing op_endpoint to the
|
|
615
|
+
# #Server constructor makes this optional.
|
|
399
616
|
#
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
617
|
+
# When an OpenID 1.x immediate mode request does
|
|
618
|
+
# not succeed, it gets back a URL where the request
|
|
619
|
+
# may be carried out in a not-so-immediate fashion.
|
|
620
|
+
# Pass my URL in here (the fully qualified address
|
|
621
|
+
# of this server's endpoint, i.e.
|
|
622
|
+
# <tt>http://example.com/server</tt>), and I will
|
|
623
|
+
# use it as a base for the URL for a new request.
|
|
404
624
|
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
|
|
625
|
+
# Optional for requests where
|
|
626
|
+
# #CheckIDRequest.immediate is false or +allow+ is
|
|
627
|
+
# true.
|
|
628
|
+
#
|
|
629
|
+
# identity:: The OP-local identifier to answer with. Only for use
|
|
630
|
+
# when the relying party requested identifier selection.
|
|
631
|
+
#
|
|
632
|
+
# claimed_id:: The claimed identifier to answer with,
|
|
633
|
+
# for use with identifier selection in the case where the
|
|
634
|
+
# claimed identifier and the OP-local identifier differ,
|
|
635
|
+
# i.e. when the claimed_id uses delegation.
|
|
636
|
+
#
|
|
637
|
+
# If +identity+ is provided but this is not,
|
|
638
|
+
# +claimed_id+ will default to the value of +identity+.
|
|
639
|
+
# When answering requests that did not ask for identifier
|
|
640
|
+
# selection, the response +claimed_id+ will default to
|
|
641
|
+
# that of the request.
|
|
642
|
+
#
|
|
643
|
+
# This parameter is new in OpenID 2.0.
|
|
644
|
+
#
|
|
645
|
+
# Version 2.0 deprecates +server_url+ and adds +claimed_id+.
|
|
646
|
+
def answer(allow, server_url=nil, identity=nil, claimed_id=nil)
|
|
647
|
+
# FIXME: undocumented exceptions
|
|
648
|
+
if !@return_to
|
|
649
|
+
raise NoReturnToError
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
if !server_url
|
|
653
|
+
if @namespace != OPENID1_NS and !@op_endpoint
|
|
654
|
+
# In other words, that warning I raised in
|
|
655
|
+
# Server.__init__? You should pay attention to it now.
|
|
656
|
+
raise RuntimeError, ("#{self} should be constructed with "\
|
|
657
|
+
"op_endpoint to respond to OpenID 2.0 "\
|
|
658
|
+
"messages.")
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
server_url = @op_endpoint
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
if allow
|
|
412
665
|
mode = 'id_res'
|
|
666
|
+
elsif @namespace == OPENID1_NS
|
|
667
|
+
if @immediate
|
|
668
|
+
mode = 'id_res'
|
|
669
|
+
else
|
|
670
|
+
mode = 'cancel'
|
|
671
|
+
end
|
|
413
672
|
else
|
|
414
|
-
|
|
673
|
+
if @immediate
|
|
674
|
+
mode = 'setup_needed'
|
|
675
|
+
else
|
|
676
|
+
mode = 'cancel'
|
|
677
|
+
end
|
|
415
678
|
end
|
|
416
679
|
|
|
417
680
|
response = OpenIDResponse.new(self)
|
|
418
681
|
|
|
682
|
+
if claimed_id and @namespace == OPENID1_NS
|
|
683
|
+
raise VersionError, ("claimed_id is new in OpenID 2.0 and not "\
|
|
684
|
+
"available for #{@namespace}")
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
if identity and !claimed_id
|
|
688
|
+
claimed_id = identity
|
|
689
|
+
end
|
|
690
|
+
|
|
419
691
|
if allow
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
692
|
+
if @identity == IDENTIFIER_SELECT
|
|
693
|
+
if !identity
|
|
694
|
+
raise ArgumentError, ("This request uses IdP-driven "\
|
|
695
|
+
"identifier selection.You must supply "\
|
|
696
|
+
"an identifier in the response.")
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
response_identity = identity
|
|
700
|
+
response_claimed_id = claimed_id
|
|
701
|
+
|
|
702
|
+
elsif @identity
|
|
703
|
+
if identity and (@identity != identity)
|
|
704
|
+
raise ArgumentError, ("Request was for identity #{@identity}, "\
|
|
705
|
+
"cannot reply with identity #{identity}")
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
response_identity = @identity
|
|
709
|
+
response_claimed_id = @claimed_id
|
|
710
|
+
else
|
|
711
|
+
if identity
|
|
712
|
+
raise ArgumentError, ("This request specified no identity "\
|
|
713
|
+
"and you supplied #{identity}")
|
|
714
|
+
end
|
|
715
|
+
response_identity = nil
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
if @namespace == OPENID1_NS and !response_identity
|
|
719
|
+
raise ArgumentError, ("Request was an OpenID 1 request, so "\
|
|
720
|
+
"response must include an identifier.")
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
response.fields.update_args(OPENID_NS, {
|
|
724
|
+
'mode' => mode,
|
|
725
|
+
'op_endpoint' => server_url,
|
|
726
|
+
'return_to' => @return_to,
|
|
727
|
+
'response_nonce' => Nonce.mk_nonce(),
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
if response_identity
|
|
731
|
+
response.fields.set_arg(OPENID_NS, 'identity', response_identity)
|
|
732
|
+
if @namespace == OPENID2_NS
|
|
733
|
+
response.fields.set_arg(OPENID_NS,
|
|
734
|
+
'claimed_id', response_claimed_id)
|
|
735
|
+
end
|
|
736
|
+
end
|
|
425
737
|
else
|
|
426
|
-
response.
|
|
427
|
-
response.signed.clear
|
|
738
|
+
response.fields.set_arg(OPENID_NS, 'mode', mode)
|
|
428
739
|
if @immediate
|
|
429
|
-
|
|
430
|
-
raise ArgumentError, "setup_url is required for allow=false
|
|
740
|
+
if @namespace == OPENID1_NS and !server_url
|
|
741
|
+
raise ArgumentError, ("setup_url is required for allow=false "\
|
|
742
|
+
"in OpenID 1.x immediate mode.")
|
|
431
743
|
end
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@trust_root
|
|
744
|
+
|
|
745
|
+
# Make a new request just like me, but with
|
|
746
|
+
# immediate=false.
|
|
747
|
+
setup_request = self.class.new(@identity, @return_to,
|
|
748
|
+
@op_endpoint, @trust_root, false,
|
|
749
|
+
@assoc_handle)
|
|
437
750
|
setup_url = setup_request.encode_to_url(server_url)
|
|
438
|
-
response.
|
|
751
|
+
response.fields.set_arg(OPENID_NS, 'user_setup_url', setup_url)
|
|
439
752
|
end
|
|
440
|
-
|
|
441
753
|
end
|
|
442
|
-
|
|
754
|
+
|
|
443
755
|
return response
|
|
444
756
|
end
|
|
445
757
|
|
|
446
|
-
# Encode this request as a GET URL, returning the URL.
|
|
447
758
|
def encode_to_url(server_url)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
q['openid.assoc_handle'] = @assoc_handle if @assoc_handle
|
|
759
|
+
# Encode this request as a URL to GET.
|
|
760
|
+
#
|
|
761
|
+
# server_url:: The URL of the OpenID server to make this
|
|
762
|
+
# request of.
|
|
763
|
+
if !@return_to
|
|
764
|
+
raise NoReturnToError
|
|
765
|
+
end
|
|
456
766
|
|
|
457
|
-
#
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
767
|
+
# Imported from the alternate reality where these classes are
|
|
768
|
+
# used in both the client and server code, so Requests are
|
|
769
|
+
# Encodable too. That's right, code imported from alternate
|
|
770
|
+
# realities all for the love of you, id_res/user_setup_url.
|
|
771
|
+
q = {'mode' => @mode,
|
|
772
|
+
'identity' => @identity,
|
|
773
|
+
'claimed_id' => @claimed_id,
|
|
774
|
+
'return_to' => @return_to}
|
|
775
|
+
|
|
776
|
+
if @trust_root
|
|
777
|
+
if @namespace == OPENID1_NS
|
|
778
|
+
q['trust_root'] = @trust_root
|
|
779
|
+
else
|
|
780
|
+
q['realm'] = @trust_root
|
|
461
781
|
end
|
|
462
782
|
end
|
|
463
783
|
|
|
464
|
-
|
|
784
|
+
if @assoc_handle
|
|
785
|
+
q['assoc_handle'] = @assoc_handle
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
response = Message.new(@namespace)
|
|
789
|
+
response.update_args(@namespace, q)
|
|
790
|
+
return response.to_url(server_url)
|
|
465
791
|
end
|
|
466
792
|
|
|
467
|
-
# Create the URL to cancel this request. Useful for creating a "Cancel"
|
|
468
|
-
# button on your "approve this openid transaction" form.
|
|
469
793
|
def cancel_url
|
|
794
|
+
# Get the URL to cancel this request.
|
|
795
|
+
#
|
|
796
|
+
# Useful for creating a "Cancel" button on a web form so that
|
|
797
|
+
# operation can be carried out directly without another trip
|
|
798
|
+
# through the server.
|
|
799
|
+
#
|
|
800
|
+
# (Except you may want to make another trip through the
|
|
801
|
+
# server so that it knows that the user did make a decision.)
|
|
802
|
+
#
|
|
803
|
+
# Returns a URL as a string.
|
|
804
|
+
if !@return_to
|
|
805
|
+
raise NoReturnToError
|
|
806
|
+
end
|
|
807
|
+
|
|
470
808
|
if @immediate
|
|
471
|
-
raise
|
|
809
|
+
raise ArgumentError.new("Cancel is not an appropriate response to " +
|
|
810
|
+
"immediate mode requests.")
|
|
472
811
|
end
|
|
473
|
-
return OpenID::Util.append_args(@return_to,{'openid.mode' => 'cancel'})
|
|
474
|
-
end
|
|
475
812
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
# "Owning" an identity_url is in the details of the
|
|
480
|
-
# server account name to URL mapping.
|
|
481
|
-
def identity_url
|
|
482
|
-
@identity
|
|
813
|
+
response = Message.new(@namespace)
|
|
814
|
+
response.set_arg(OPENID_NS, 'mode', 'cancel')
|
|
815
|
+
return response.to_url(@return_to)
|
|
483
816
|
end
|
|
484
817
|
|
|
818
|
+
def to_s
|
|
819
|
+
return sprintf('<%s id:%s im:%s tr:%s ah:%s>', self.class,
|
|
820
|
+
@identity,
|
|
821
|
+
@immediate,
|
|
822
|
+
@trust_root,
|
|
823
|
+
@assoc_handle)
|
|
824
|
+
end
|
|
485
825
|
end
|
|
486
|
-
|
|
487
|
-
#
|
|
826
|
+
|
|
827
|
+
# I am a response to an OpenID request.
|
|
828
|
+
#
|
|
829
|
+
# Attributes:
|
|
830
|
+
# signed:: A list of the names of the fields which should be signed.
|
|
831
|
+
#
|
|
832
|
+
# Implementer's note: In a more symmetric client/server
|
|
833
|
+
# implementation, there would be more types of #OpenIDResponse
|
|
834
|
+
# object and they would have validated attributes according to
|
|
835
|
+
# the type of response. But as it is, Response objects in a
|
|
836
|
+
# server are basically write-only, their only job is to go out
|
|
837
|
+
# over the wire, so this is just a loose wrapper around
|
|
838
|
+
# #OpenIDResponse.fields.
|
|
488
839
|
class OpenIDResponse
|
|
840
|
+
# The #OpenIDRequest I respond to.
|
|
841
|
+
attr_accessor :request
|
|
489
842
|
|
|
490
|
-
|
|
843
|
+
# An #OpenID::Message with the data to be returned.
|
|
844
|
+
# Keys are parameter names with no
|
|
845
|
+
# leading openid. e.g. identity and mac_key
|
|
846
|
+
# never openid.identity.
|
|
847
|
+
attr_accessor :fields
|
|
491
848
|
|
|
492
|
-
# +request+ is a subclass of OpenIDRequest that this object
|
|
493
|
-
# should respond to.
|
|
494
849
|
def initialize(request)
|
|
850
|
+
# Make a response to an OpenIDRequest.
|
|
495
851
|
@request = request
|
|
496
|
-
@fields =
|
|
497
|
-
@signed = []
|
|
852
|
+
@fields = Message.new(request.namespace)
|
|
498
853
|
end
|
|
499
854
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
# pass 'sreg' as the namespace.
|
|
506
|
-
def add_field(namespace, key, value, signed=true)
|
|
507
|
-
if namespace and namespace != ''
|
|
508
|
-
key = namespace + '.' + key
|
|
509
|
-
end
|
|
510
|
-
@fields[key] = value
|
|
511
|
-
if signed and not @signed.member?(key)
|
|
512
|
-
@signed << key
|
|
513
|
-
end
|
|
855
|
+
def to_s
|
|
856
|
+
return sprintf("%s for %s: %s",
|
|
857
|
+
self.class,
|
|
858
|
+
@request.class,
|
|
859
|
+
@fields)
|
|
514
860
|
end
|
|
515
861
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
862
|
+
def to_form_markup
|
|
863
|
+
# Returns the form markup for this response.
|
|
864
|
+
return @fields.to_form_markup(
|
|
865
|
+
@fields.get_arg(OPENID_NS, 'return_to'))
|
|
520
866
|
end
|
|
521
867
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
other.fields.each {|k,v| namespaced_fields[namespace+'.'+k] = v}
|
|
527
|
-
namespaced_signed = other.signed.collect {|k| namespace+'.'+k}
|
|
528
|
-
else
|
|
529
|
-
namespaced_fields = other.fields.dup
|
|
530
|
-
namespaced_signed = other.signed.dup
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
@fields.update(namespaced_fields)
|
|
534
|
-
@signed |= namespaced_signed
|
|
868
|
+
def render_as_form
|
|
869
|
+
# Returns true if this response's encoding is
|
|
870
|
+
# ENCODE_HTML_FORM. Convenience method for server authors.
|
|
871
|
+
return self.which_encoding == ENCODE_HTML_FORM
|
|
535
872
|
end
|
|
536
873
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
['checkid_immediate','checkid_setup'].member?(@request.mode) and \
|
|
541
|
-
@signed.length > 0
|
|
874
|
+
def needs_signing
|
|
875
|
+
# Does this response require signing?
|
|
876
|
+
return @fields.get_arg(OPENID_NS, 'mode') == 'id_res'
|
|
542
877
|
end
|
|
543
878
|
|
|
544
|
-
#
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
879
|
+
# implements IEncodable
|
|
880
|
+
|
|
881
|
+
def which_encoding
|
|
882
|
+
# How should I be encoded?
|
|
883
|
+
# returns one of ENCODE_URL or ENCODE_KVFORM.
|
|
549
884
|
if BROWSER_REQUEST_MODES.member?(@request.mode)
|
|
550
|
-
|
|
885
|
+
if @fields.get_openid_namespace == OPENID2_NS and
|
|
886
|
+
encode_to_url.length > OPENID1_URL_LIMIT
|
|
887
|
+
return ENCODE_HTML_FORM
|
|
888
|
+
else
|
|
889
|
+
return ENCODE_URL
|
|
890
|
+
end
|
|
551
891
|
else
|
|
552
892
|
return ENCODE_KVFORM
|
|
553
893
|
end
|
|
554
894
|
end
|
|
555
895
|
|
|
556
|
-
# Encode the response to a URL, suitable to be send via 302 redirect.
|
|
557
896
|
def encode_to_url
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return
|
|
897
|
+
# Encode a response as a URL for the user agent to GET.
|
|
898
|
+
# You will generally use this URL with a HTTP redirect.
|
|
899
|
+
return @fields.to_url(@request.return_to)
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def add_extension(extension_response)
|
|
903
|
+
# Add an extension response to this response message.
|
|
904
|
+
#
|
|
905
|
+
# extension_response:: An object that implements the
|
|
906
|
+
# #OpenID::Extension interface for adding arguments to an OpenID
|
|
907
|
+
# message.
|
|
908
|
+
extension_response.to_message(@fields)
|
|
561
909
|
end
|
|
562
910
|
|
|
563
|
-
# Encode the response to kvform format.
|
|
564
911
|
def encode_to_kvform
|
|
565
|
-
|
|
912
|
+
# Encode a response in key-value colon/newline format.
|
|
913
|
+
#
|
|
914
|
+
# This is a machine-readable format used to respond to
|
|
915
|
+
# messages which came directly from the consumer and not
|
|
916
|
+
# through the user agent.
|
|
917
|
+
#
|
|
918
|
+
# see: OpenID Specs,
|
|
919
|
+
# <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a>
|
|
920
|
+
return @fields.to_kvform
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def copy
|
|
924
|
+
return Marshal.load(Marshal.dump(self))
|
|
566
925
|
end
|
|
567
|
-
|
|
568
926
|
end
|
|
569
927
|
|
|
570
|
-
#
|
|
928
|
+
# I am a response to an OpenID request in terms a web server
|
|
929
|
+
# understands.
|
|
930
|
+
#
|
|
931
|
+
# I generally come from an #Encoder, either directly or from
|
|
932
|
+
# #Server.encodeResponse.
|
|
933
|
+
class WebResponse
|
|
934
|
+
|
|
935
|
+
# The HTTP code of this response as an integer.
|
|
936
|
+
attr_accessor :code
|
|
937
|
+
|
|
938
|
+
# #Hash of headers to include in this response.
|
|
939
|
+
attr_accessor :headers
|
|
940
|
+
|
|
941
|
+
# The body of this response.
|
|
942
|
+
attr_accessor :body
|
|
943
|
+
|
|
944
|
+
def initialize(code=HTTP_OK, headers=nil, body="")
|
|
945
|
+
# Construct me.
|
|
946
|
+
#
|
|
947
|
+
# These parameters are assigned directly as class attributes,
|
|
948
|
+
# see my class documentation for their
|
|
949
|
+
# descriptions.
|
|
950
|
+
@code = code
|
|
951
|
+
if headers
|
|
952
|
+
@headers = headers
|
|
953
|
+
else
|
|
954
|
+
@headers = {}
|
|
955
|
+
end
|
|
956
|
+
@body = body
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
# I sign things.
|
|
961
|
+
#
|
|
962
|
+
# I also check signatures.
|
|
963
|
+
#
|
|
964
|
+
# All my state is encapsulated in a store, which means I'm not
|
|
965
|
+
# generally pickleable but I am easy to reconstruct.
|
|
571
966
|
class Signatory
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
#
|
|
579
|
-
|
|
967
|
+
# The number of seconds a secret remains valid. Defaults to 14 days.
|
|
968
|
+
attr_accessor :secret_lifetime
|
|
969
|
+
|
|
970
|
+
# keys have a bogus server URL in them because the filestore
|
|
971
|
+
# really does expect that key to be a URL. This seems a little
|
|
972
|
+
# silly for the server store, since I expect there to be only
|
|
973
|
+
# one server URL.
|
|
974
|
+
@@_normal_key = 'http://localhost/|normal'
|
|
975
|
+
@@_dumb_key = 'http://localhost/|dumb'
|
|
976
|
+
|
|
977
|
+
def self._normal_key
|
|
978
|
+
@@_normal_key
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def self._dumb_key
|
|
982
|
+
@@_dumb_key
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
attr_accessor :store
|
|
986
|
+
|
|
987
|
+
# Create a new Signatory. store is The back-end where my
|
|
988
|
+
# associations are stored.
|
|
580
989
|
def initialize(store)
|
|
990
|
+
Util.assert(store)
|
|
581
991
|
@store = store
|
|
992
|
+
@secret_lifetime = 14 * 24 * 60 * 60
|
|
582
993
|
end
|
|
583
994
|
|
|
584
995
|
# Verify that the signature for some data is valid.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
# passed in via openid.assoc_handle.
|
|
591
|
-
#
|
|
592
|
-
# [+sig+]
|
|
593
|
-
# Value of openid.sig
|
|
594
|
-
#
|
|
595
|
-
# [+signed_pairs+]
|
|
596
|
-
# Array of Array pairs of key, value signed data.
|
|
597
|
-
#
|
|
598
|
-
# [+dumb+]
|
|
599
|
-
# boolean representing whether this is a dumb mode request
|
|
600
|
-
def verify(assoc_handle, sig, signed_pairs, dumb=true)
|
|
601
|
-
assoc = self.get_association(assoc_handle, dumb)
|
|
602
|
-
unless assoc
|
|
603
|
-
OpenID::Util.log("failed to get assoc with handle #{assoc_handle} to verify sig #{sig}")
|
|
996
|
+
def verify(assoc_handle, message)
|
|
997
|
+
assoc = get_association(assoc_handle, true)
|
|
998
|
+
if !assoc
|
|
999
|
+
Util.log(sprintf("failed to get assoc with handle %s to verify " +
|
|
1000
|
+
"message %s", assoc_handle, message))
|
|
604
1001
|
return false
|
|
605
1002
|
end
|
|
606
|
-
|
|
607
|
-
expected_sig = OpenID::Util.to_base64(assoc.sign(signed_pairs))
|
|
608
1003
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1004
|
+
begin
|
|
1005
|
+
valid = assoc.check_message_signature(message)
|
|
1006
|
+
rescue StandardError => ex
|
|
1007
|
+
Util.log(sprintf("Error in verifying %s with %s: %s",
|
|
1008
|
+
message, assoc, ex))
|
|
613
1009
|
return false
|
|
614
1010
|
end
|
|
1011
|
+
|
|
1012
|
+
return valid
|
|
615
1013
|
end
|
|
616
1014
|
|
|
617
|
-
# Sign a response
|
|
618
|
-
#
|
|
619
|
-
#
|
|
1015
|
+
# Sign a response.
|
|
1016
|
+
#
|
|
1017
|
+
# I take an OpenIDResponse, create a signature for everything in
|
|
1018
|
+
# its signed list, and return a new copy of the response object
|
|
1019
|
+
# with that signature included.
|
|
620
1020
|
def sign(response)
|
|
621
|
-
|
|
622
|
-
signed_response = Marshal.load(Marshal.dump(response))
|
|
1021
|
+
signed_response = response.copy
|
|
623
1022
|
assoc_handle = response.request.assoc_handle
|
|
624
|
-
|
|
625
1023
|
if assoc_handle
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1024
|
+
# normal mode disabling expiration check because even if the
|
|
1025
|
+
# association is expired, we still need to know some
|
|
1026
|
+
# properties of the association so that we may preserve
|
|
1027
|
+
# those properties when creating the fallback association.
|
|
1028
|
+
assoc = get_association(assoc_handle, false, false)
|
|
1029
|
+
|
|
1030
|
+
if !assoc or assoc.expires_in <= 0
|
|
1031
|
+
# fall back to dumb mode
|
|
1032
|
+
signed_response.fields.set_arg(
|
|
1033
|
+
OPENID_NS, 'invalidate_handle', assoc_handle)
|
|
1034
|
+
assoc_type = assoc ? assoc.assoc_type : 'HMAC-SHA1'
|
|
1035
|
+
if assoc and assoc.expires_in <= 0
|
|
1036
|
+
# now do the clean-up that the disabled checkExpiration
|
|
1037
|
+
# code didn't get to do.
|
|
1038
|
+
invalidate(assoc_handle, false)
|
|
1039
|
+
end
|
|
1040
|
+
assoc = create_association(true, assoc_type)
|
|
631
1041
|
end
|
|
632
1042
|
else
|
|
633
|
-
# dumb mode
|
|
634
|
-
assoc =
|
|
1043
|
+
# dumb mode.
|
|
1044
|
+
assoc = create_association(true)
|
|
635
1045
|
end
|
|
636
|
-
|
|
637
|
-
signed_response.fields
|
|
638
|
-
assoc.add_signature(signed_response.signed,
|
|
639
|
-
signed_response.fields, '')
|
|
1046
|
+
|
|
1047
|
+
signed_response.fields = assoc.sign_message(signed_response.fields)
|
|
640
1048
|
return signed_response
|
|
641
1049
|
end
|
|
642
1050
|
|
|
643
|
-
# Make a new
|
|
1051
|
+
# Make a new association.
|
|
644
1052
|
def create_association(dumb=true, assoc_type='HMAC-SHA1')
|
|
645
|
-
secret = OpenID
|
|
646
|
-
uniq =
|
|
647
|
-
handle =
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1053
|
+
secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type))
|
|
1054
|
+
uniq = Util.to_base64(CryptUtil.random_string(4))
|
|
1055
|
+
handle = sprintf('{%s}{%x}{%s}', assoc_type, Time.now.to_i, uniq)
|
|
1056
|
+
|
|
1057
|
+
assoc = Association.from_expires_in(
|
|
1058
|
+
secret_lifetime, handle, secret, assoc_type)
|
|
1059
|
+
|
|
1060
|
+
if dumb
|
|
1061
|
+
key = @@_dumb_key
|
|
1062
|
+
else
|
|
1063
|
+
key = @@_normal_key
|
|
1064
|
+
end
|
|
1065
|
+
|
|
654
1066
|
@store.store_association(key, assoc)
|
|
655
1067
|
return assoc
|
|
656
1068
|
end
|
|
657
1069
|
|
|
658
|
-
# Get
|
|
659
|
-
def get_association(assoc_handle, dumb)
|
|
660
|
-
|
|
661
|
-
|
|
1070
|
+
# Get the association with the specified handle.
|
|
1071
|
+
def get_association(assoc_handle, dumb, checkExpiration=true)
|
|
1072
|
+
# Hmm. We've created an interface that deals almost entirely
|
|
1073
|
+
# with assoc_handles. The only place outside the Signatory
|
|
1074
|
+
# that uses this (and thus the only place that ever sees
|
|
1075
|
+
# Association objects) is when creating a response to an
|
|
1076
|
+
# association request, as it must have the association's
|
|
1077
|
+
# secret.
|
|
1078
|
+
|
|
1079
|
+
if !assoc_handle
|
|
1080
|
+
raise ArgumentError.new("assoc_handle must not be None")
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
if dumb
|
|
1084
|
+
key = @@_dumb_key
|
|
1085
|
+
else
|
|
1086
|
+
key = @@_normal_key
|
|
662
1087
|
end
|
|
663
|
-
|
|
664
|
-
key = dumb ? @@dumb_key : @@normal_key
|
|
665
|
-
|
|
1088
|
+
|
|
666
1089
|
assoc = @store.get_association(key, assoc_handle)
|
|
667
|
-
if assoc and assoc.
|
|
668
|
-
|
|
669
|
-
|
|
1090
|
+
if assoc and assoc.expires_in <= 0
|
|
1091
|
+
Util.log(sprintf("requested %sdumb key %s is expired (by %s seconds)",
|
|
1092
|
+
(!dumb) ? 'not-' : '',
|
|
1093
|
+
assoc_handle, assoc.expires_in))
|
|
1094
|
+
if checkExpiration
|
|
1095
|
+
@store.remove_association(key, assoc_handle)
|
|
1096
|
+
assoc = nil
|
|
1097
|
+
end
|
|
670
1098
|
end
|
|
671
|
-
|
|
1099
|
+
|
|
672
1100
|
return assoc
|
|
673
1101
|
end
|
|
674
1102
|
|
|
675
|
-
#
|
|
1103
|
+
# Invalidates the association with the given handle.
|
|
676
1104
|
def invalidate(assoc_handle, dumb)
|
|
677
|
-
|
|
1105
|
+
if dumb
|
|
1106
|
+
key = @@_dumb_key
|
|
1107
|
+
else
|
|
1108
|
+
key = @@_normal_key
|
|
1109
|
+
end
|
|
1110
|
+
|
|
678
1111
|
@store.remove_association(key, assoc_handle)
|
|
679
1112
|
end
|
|
680
|
-
|
|
681
1113
|
end
|
|
682
1114
|
|
|
683
|
-
#
|
|
684
|
-
#
|
|
685
|
-
#
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
@headers = {}
|
|
693
|
-
@body = ''
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
def set_redirect(url)
|
|
697
|
-
@code = HTTP_REDIRECT
|
|
698
|
-
@headers['location'] = url
|
|
699
|
-
end
|
|
1115
|
+
# I encode responses in to WebResponses.
|
|
1116
|
+
#
|
|
1117
|
+
# If you don't like WebResponses, you can do
|
|
1118
|
+
# your own handling of OpenIDResponses with
|
|
1119
|
+
# OpenIDResponse.whichEncoding,
|
|
1120
|
+
# OpenIDResponse.encodeToURL, and
|
|
1121
|
+
# OpenIDResponse.encodeToKVForm.
|
|
1122
|
+
class Encoder
|
|
1123
|
+
@@responseFactory = WebResponse
|
|
700
1124
|
|
|
701
|
-
#
|
|
702
|
-
# webserver to the value returned by the redirect_url method.
|
|
1125
|
+
# Encode a response to a WebResponse.
|
|
703
1126
|
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
def is_redirect?
|
|
707
|
-
@code == HTTP_REDIRECT
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
def redirect_url
|
|
711
|
-
@headers['location']
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
# Object that encodes OpenIDResponse objects into WebResponse objects.
|
|
717
|
-
class Encoder
|
|
718
|
-
|
|
1127
|
+
# Raises EncodingError when I can't figure out how to encode
|
|
1128
|
+
# this message.
|
|
719
1129
|
def encode(response)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1130
|
+
encode_as = response.which_encoding()
|
|
1131
|
+
if encode_as == ENCODE_KVFORM
|
|
1132
|
+
wr = @@responseFactory.new(HTTP_OK, nil,
|
|
1133
|
+
response.encode_to_kvform())
|
|
1134
|
+
if response.is_a?(Exception)
|
|
1135
|
+
wr.code = HTTP_ERROR
|
|
1136
|
+
end
|
|
1137
|
+
elsif encode_as == ENCODE_URL
|
|
1138
|
+
location = response.encode_to_url()
|
|
1139
|
+
wr = @@responseFactory.new(HTTP_REDIRECT,
|
|
1140
|
+
{'location' => location})
|
|
1141
|
+
elsif encode_as == ENCODE_HTML_FORM
|
|
1142
|
+
wr = @@responseFactory.new(HTTP_OK, nil,
|
|
1143
|
+
response.to_form_markup())
|
|
730
1144
|
else
|
|
731
|
-
#
|
|
1145
|
+
# Can't encode this to a protocol message. You should
|
|
1146
|
+
# probably render it to HTML and show it to the user.
|
|
732
1147
|
raise EncodingError.new(response)
|
|
733
1148
|
end
|
|
734
|
-
|
|
735
|
-
return webresponse
|
|
736
|
-
end
|
|
737
1149
|
|
|
1150
|
+
return wr
|
|
1151
|
+
end
|
|
738
1152
|
end
|
|
739
1153
|
|
|
740
|
-
#
|
|
741
|
-
#
|
|
1154
|
+
# I encode responses in to WebResponses, signing
|
|
1155
|
+
# them when required.
|
|
742
1156
|
class SigningEncoder < Encoder
|
|
743
|
-
|
|
1157
|
+
|
|
1158
|
+
attr_accessor :signatory
|
|
1159
|
+
|
|
1160
|
+
# Create a SigningEncoder given a Signatory
|
|
744
1161
|
def initialize(signatory)
|
|
745
|
-
if signatory.nil?
|
|
746
|
-
raise ArgumentError, "signatory must not be nil"
|
|
747
|
-
end
|
|
748
1162
|
@signatory = signatory
|
|
749
1163
|
end
|
|
750
1164
|
|
|
1165
|
+
# Encode a response to a WebResponse, signing it first if
|
|
1166
|
+
# appropriate.
|
|
1167
|
+
#
|
|
1168
|
+
# Raises EncodingError when I can't figure out how to encode this
|
|
1169
|
+
# message.
|
|
1170
|
+
#
|
|
1171
|
+
# Raises AlreadySigned when this response is already signed.
|
|
751
1172
|
def encode(response)
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1173
|
+
# the is_a? is a bit of a kludge... it means there isn't
|
|
1174
|
+
# really an adapter to make the interfaces quite match.
|
|
1175
|
+
if !response.is_a?(Exception) and response.needs_signing()
|
|
1176
|
+
if !@signatory
|
|
1177
|
+
raise ArgumentError.new(
|
|
1178
|
+
sprintf("Must have a store to sign this request: %s",
|
|
1179
|
+
response), response)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
if response.fields.has_key?(OPENID_NS, 'sig')
|
|
1183
|
+
raise AlreadySigned.new(response)
|
|
755
1184
|
end
|
|
1185
|
+
|
|
756
1186
|
response = @signatory.sign(response)
|
|
757
1187
|
end
|
|
1188
|
+
|
|
758
1189
|
return super(response)
|
|
759
1190
|
end
|
|
760
|
-
|
|
761
1191
|
end
|
|
762
1192
|
|
|
763
|
-
#
|
|
1193
|
+
# I decode an incoming web request in to a OpenIDRequest.
|
|
764
1194
|
class Decoder
|
|
765
1195
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1196
|
+
@@handlers = {
|
|
1197
|
+
'checkid_setup' => CheckIDRequest.method('from_message'),
|
|
1198
|
+
'checkid_immediate' => CheckIDRequest.method('from_message'),
|
|
1199
|
+
'check_authentication' => CheckAuthRequest.method('from_message'),
|
|
1200
|
+
'associate' => AssociateRequest.method('from_message'),
|
|
1201
|
+
}
|
|
771
1202
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1203
|
+
attr_accessor :server
|
|
1204
|
+
|
|
1205
|
+
# Construct a Decoder. The server is necessary because some
|
|
1206
|
+
# replies reference their server.
|
|
1207
|
+
def initialize(server)
|
|
1208
|
+
@server = server
|
|
1209
|
+
end
|
|
775
1210
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1211
|
+
# I transform query parameters into an OpenIDRequest.
|
|
1212
|
+
#
|
|
1213
|
+
# If the query does not seem to be an OpenID request at all, I
|
|
1214
|
+
# return nil.
|
|
1215
|
+
#
|
|
1216
|
+
# Raises ProtocolError when the query does not seem to be a valid
|
|
1217
|
+
# OpenID request.
|
|
1218
|
+
def decode(query)
|
|
1219
|
+
if query.nil? or query.length == 0
|
|
1220
|
+
return nil
|
|
1221
|
+
end
|
|
779
1222
|
|
|
780
|
-
|
|
781
|
-
return CheckAuthRequest.from_query(query)
|
|
1223
|
+
message = Message.from_post_args(query)
|
|
782
1224
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
raise ProtocolError.new(query, "Unknown mode #{mode}")
|
|
1225
|
+
mode = message.get_arg(OPENID_NS, 'mode')
|
|
1226
|
+
if !mode
|
|
1227
|
+
msg = sprintf("No mode value in message %s", message)
|
|
1228
|
+
raise ProtocolError.new(message, msg)
|
|
788
1229
|
end
|
|
789
|
-
|
|
1230
|
+
|
|
1231
|
+
handler = @@handlers.fetch(mode, self.method('default_decoder'))
|
|
1232
|
+
return handler.call(message, @server.op_endpoint)
|
|
790
1233
|
end
|
|
791
1234
|
|
|
1235
|
+
# Called to decode queries when no handler for that mode is
|
|
1236
|
+
# found.
|
|
1237
|
+
#
|
|
1238
|
+
# This implementation always raises ProtocolError.
|
|
1239
|
+
def default_decoder(message, server)
|
|
1240
|
+
mode = message.get_arg(OPENID_NS, 'mode')
|
|
1241
|
+
msg = sprintf("No decoder for mode %s", mode)
|
|
1242
|
+
raise ProtocolError.new(message, msg)
|
|
1243
|
+
end
|
|
792
1244
|
end
|
|
793
1245
|
|
|
794
|
-
#
|
|
795
|
-
#
|
|
796
|
-
# Some types of requests (those which are not CheckIDRequest objects) may
|
|
797
|
-
# be handed to the handle_request method, and an appropriate response
|
|
798
|
-
# will be returned.
|
|
799
|
-
#
|
|
800
|
-
# For convenienve, decode and encode methods are exposed which should be
|
|
801
|
-
# used as the entry and exit points of the OpenID server logic. The first
|
|
802
|
-
# step when handling an OpenID server action should be to call
|
|
803
|
-
# Server.decode_request with the query arguments.
|
|
1246
|
+
# I handle requests for an OpenID server.
|
|
804
1247
|
#
|
|
805
|
-
#
|
|
806
|
-
#
|
|
807
|
-
#
|
|
1248
|
+
# Some types of requests (those which are not checkid requests)
|
|
1249
|
+
# may be handed to my handleRequest method, and I will take care
|
|
1250
|
+
# of it and return a response.
|
|
808
1251
|
#
|
|
809
|
-
#
|
|
810
|
-
#
|
|
811
|
-
#
|
|
812
|
-
# requests. The +params+ variable represents a Hash of the incoming
|
|
813
|
-
# arguments. is_authorized and show_decide_page are methods you provide.
|
|
814
|
-
# At the end you have a WebResponse object suitable for examining and
|
|
815
|
-
# issuing a response to your web server.
|
|
816
|
-
#
|
|
817
|
-
# include OpenID
|
|
818
|
-
# store = FilesystemStore.new('/var/openid/store')
|
|
819
|
-
# server = Server::Server.new(store)
|
|
820
|
-
# request = server.decode_request(params)
|
|
821
|
-
# if request.kind_of?(CheckIDRequest)
|
|
822
|
-
# if is_authorized(request.identity, request.trust_root)
|
|
823
|
-
# response = request.answer(true)
|
|
824
|
-
# elsif request.immediate
|
|
825
|
-
# response = request.answer(false,'http://example.com/openid-server')
|
|
826
|
-
# else
|
|
827
|
-
# show_decide_page(request)
|
|
828
|
-
# return
|
|
829
|
-
# end
|
|
830
|
-
# else
|
|
831
|
-
# response = server.handle_request(request)
|
|
832
|
-
# end
|
|
1252
|
+
# For your convenience, I also provide an interface to
|
|
1253
|
+
# Decoder.decode and SigningEncoder.encode through my methods
|
|
1254
|
+
# decodeRequest and encodeResponse.
|
|
833
1255
|
#
|
|
834
|
-
#
|
|
835
|
-
#
|
|
836
|
-
# For an actual working example, please see the rails_server
|
|
837
|
-
# directory inside of the examples directory. Have a look at the
|
|
838
|
-
# app/controllers/server_controller.rb and the +index+ method of the
|
|
839
|
-
# ServerController object.
|
|
1256
|
+
# All my state is encapsulated in an store, which means I'm not
|
|
1257
|
+
# generally pickleable but I am easy to reconstruct.
|
|
840
1258
|
class Server
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
@store = store
|
|
845
|
-
@signatory = Signatory.new(store)
|
|
846
|
-
@encoder = SigningEncoder.new(@signatory)
|
|
847
|
-
@decoder = Decoder.new
|
|
848
|
-
end
|
|
1259
|
+
@@signatoryClass = Signatory
|
|
1260
|
+
@@encoderClass = SigningEncoder
|
|
1261
|
+
@@decoderClass = Decoder
|
|
849
1262
|
|
|
850
|
-
#
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1263
|
+
# The back-end where my associations and nonces are stored.
|
|
1264
|
+
attr_accessor :store
|
|
1265
|
+
|
|
1266
|
+
# I'm using this for associate requests and to sign things.
|
|
1267
|
+
attr_accessor :signatory
|
|
1268
|
+
|
|
1269
|
+
# I'm using this to encode things.
|
|
1270
|
+
attr_accessor :encoder
|
|
1271
|
+
|
|
1272
|
+
# I'm using this to decode things.
|
|
1273
|
+
attr_accessor :decoder
|
|
1274
|
+
|
|
1275
|
+
# I use this instance of OpenID::AssociationNegotiator to
|
|
1276
|
+
# determine which kinds of associations I can make and how.
|
|
1277
|
+
attr_accessor :negotiator
|
|
1278
|
+
|
|
1279
|
+
# My URL.
|
|
1280
|
+
attr_accessor :op_endpoint
|
|
1281
|
+
|
|
1282
|
+
# op_endpoint is new in library version 2.0.
|
|
1283
|
+
def initialize(store, op_endpoint)
|
|
1284
|
+
@store = store
|
|
1285
|
+
@signatory = @@signatoryClass.new(@store)
|
|
1286
|
+
@encoder = @@encoderClass.new(@signatory)
|
|
1287
|
+
@decoder = @@decoderClass.new(self)
|
|
1288
|
+
@negotiator = DefaultNegotiator.copy()
|
|
1289
|
+
@op_endpoint = op_endpoint
|
|
855
1290
|
end
|
|
856
1291
|
|
|
857
|
-
# Handle
|
|
1292
|
+
# Handle a request.
|
|
1293
|
+
#
|
|
1294
|
+
# Give me a request, I will give you a response. Unless it's a
|
|
1295
|
+
# type of request I cannot handle myself, in which case I will
|
|
1296
|
+
# raise RuntimeError. In that case, you can handle it yourself,
|
|
1297
|
+
# or add a method to me for handling that request type.
|
|
858
1298
|
def handle_request(request)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1299
|
+
begin
|
|
1300
|
+
handler = self.method('openid_' + request.mode)
|
|
1301
|
+
rescue NameError
|
|
1302
|
+
raise RuntimeError.new(
|
|
1303
|
+
sprintf("%s has no handler for a request of mode %s.",
|
|
1304
|
+
self, request.mode))
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
return handler.call(request)
|
|
865
1308
|
end
|
|
866
|
-
|
|
867
|
-
#
|
|
1309
|
+
|
|
1310
|
+
# Handle and respond to check_authentication requests.
|
|
868
1311
|
def openid_check_authentication(request)
|
|
869
1312
|
return request.answer(@signatory)
|
|
870
1313
|
end
|
|
871
1314
|
|
|
872
|
-
#
|
|
1315
|
+
# Handle and respond to associate requests.
|
|
873
1316
|
def openid_associate(request)
|
|
874
|
-
|
|
875
|
-
|
|
1317
|
+
assoc_type = request.assoc_type
|
|
1318
|
+
session_type = request.session.session_type
|
|
1319
|
+
if @negotiator.allowed?(assoc_type, session_type)
|
|
1320
|
+
assoc = @signatory.create_association(false,
|
|
1321
|
+
assoc_type)
|
|
1322
|
+
return request.answer(assoc)
|
|
1323
|
+
else
|
|
1324
|
+
message = sprintf('Association type %s is not supported with ' +
|
|
1325
|
+
'session type %s', assoc_type, session_type)
|
|
1326
|
+
preferred_assoc_type, preferred_session_type = @negotiator.get_allowed_type()
|
|
1327
|
+
return request.answer_unsupported(message,
|
|
1328
|
+
preferred_assoc_type,
|
|
1329
|
+
preferred_session_type)
|
|
1330
|
+
end
|
|
876
1331
|
end
|
|
877
1332
|
|
|
1333
|
+
# Transform query parameters into an OpenIDRequest.
|
|
1334
|
+
# query should contain the query parameters as a Hash with
|
|
1335
|
+
# each key mapping to one value.
|
|
1336
|
+
#
|
|
1337
|
+
# If the query does not seem to be an OpenID request at all, I
|
|
1338
|
+
# return nil.
|
|
1339
|
+
def decode_request(query)
|
|
1340
|
+
return @decoder.decode(query)
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# Encode a response to a WebResponse, signing it first if
|
|
1344
|
+
# appropriate.
|
|
1345
|
+
#
|
|
1346
|
+
# Raises EncodingError when I can't figure out how to encode this
|
|
1347
|
+
# message.
|
|
1348
|
+
#
|
|
1349
|
+
# Raises AlreadySigned When this response is already signed.
|
|
1350
|
+
def encode_response(response)
|
|
1351
|
+
return @encoder.encode(response)
|
|
1352
|
+
end
|
|
878
1353
|
end
|
|
879
1354
|
|
|
880
|
-
#
|
|
1355
|
+
# A message did not conform to the OpenID protocol.
|
|
881
1356
|
class ProtocolError < Exception
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1357
|
+
# The query that is failing to be a valid OpenID request.
|
|
1358
|
+
attr_accessor :openid_message
|
|
1359
|
+
attr_accessor :reference
|
|
1360
|
+
attr_accessor :contact
|
|
1361
|
+
|
|
1362
|
+
# text:: A message about the encountered error.
|
|
1363
|
+
def initialize(message, text=nil, reference=nil, contact=nil)
|
|
1364
|
+
@openid_message = message
|
|
1365
|
+
@reference = reference
|
|
1366
|
+
@contact = contact
|
|
1367
|
+
Util.assert(!message.is_a?(String))
|
|
886
1368
|
super(text)
|
|
887
|
-
@query = query
|
|
888
1369
|
end
|
|
889
1370
|
|
|
890
|
-
|
|
891
|
-
|
|
1371
|
+
# Get the return_to argument from the request, if any.
|
|
1372
|
+
def get_return_to
|
|
1373
|
+
if @openid_message.nil?
|
|
1374
|
+
return nil
|
|
1375
|
+
else
|
|
1376
|
+
return @openid_message.get_arg(OPENID_NS, 'return_to')
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
# Did this request have a return_to parameter?
|
|
1381
|
+
def has_return_to
|
|
1382
|
+
return !get_return_to.nil?
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
# Generate a Message object for sending to the relying party,
|
|
1386
|
+
# after encoding.
|
|
1387
|
+
def to_message
|
|
1388
|
+
namespace = @openid_message.get_openid_namespace()
|
|
1389
|
+
reply = Message.new(namespace)
|
|
1390
|
+
reply.set_arg(OPENID_NS, 'mode', 'error')
|
|
1391
|
+
reply.set_arg(OPENID_NS, 'error', self.to_s)
|
|
1392
|
+
|
|
1393
|
+
if @contact
|
|
1394
|
+
reply.set_arg(OPENID_NS, 'contact', @contact.to_s)
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
if @reference
|
|
1398
|
+
reply.set_arg(OPENID_NS, 'reference', @reference.to_s)
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
return reply
|
|
892
1402
|
end
|
|
893
1403
|
|
|
1404
|
+
# implements IEncodable
|
|
1405
|
+
|
|
894
1406
|
def encode_to_url
|
|
895
|
-
|
|
896
|
-
unless return_to
|
|
897
|
-
raise ArgumentError, 'No return_to in query'
|
|
898
|
-
end
|
|
899
|
-
|
|
900
|
-
args = {
|
|
901
|
-
'openid.mode' => 'error',
|
|
902
|
-
'openid.error' => self.to_s
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
return OpenID::Util.append_args(return_to, args)
|
|
1407
|
+
return to_message().to_url(get_return_to())
|
|
906
1408
|
end
|
|
907
1409
|
|
|
908
1410
|
def encode_to_kvform
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return
|
|
1411
|
+
return to_message().to_kvform()
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
def to_form_markup
|
|
1415
|
+
return to_message().to_form_markup(get_return_to())
|
|
914
1416
|
end
|
|
915
1417
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1418
|
+
# How should I be encoded?
|
|
1419
|
+
#
|
|
1420
|
+
# Returns one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
|
|
1421
|
+
# I cannot be encoded as a protocol message and should be
|
|
1422
|
+
# displayed to the user.
|
|
1423
|
+
def which_encoding
|
|
1424
|
+
if has_return_to()
|
|
1425
|
+
if @openid_message.get_openid_namespace() == OPENID2_NS and
|
|
1426
|
+
encode_to_url().length > OPENID1_URL_LIMIT
|
|
1427
|
+
return ENCODE_HTML_FORM
|
|
1428
|
+
else
|
|
1429
|
+
return ENCODE_URL
|
|
1430
|
+
end
|
|
919
1431
|
end
|
|
920
1432
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
return ENCODE_KVFORM
|
|
1433
|
+
if @openid_message.nil?
|
|
1434
|
+
return nil
|
|
924
1435
|
end
|
|
925
|
-
|
|
1436
|
+
|
|
1437
|
+
mode = @openid_message.get_arg(OPENID_NS, 'mode')
|
|
1438
|
+
if mode
|
|
1439
|
+
if !BROWSER_REQUEST_MODES.member?(mode)
|
|
1440
|
+
return ENCODE_KVFORM
|
|
1441
|
+
end
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
# If your request was so broken that you didn't manage to
|
|
1445
|
+
# include an openid.mode, I'm not going to worry too much
|
|
1446
|
+
# about returning you something you can't parse.
|
|
926
1447
|
return nil
|
|
927
1448
|
end
|
|
1449
|
+
end
|
|
928
1450
|
|
|
1451
|
+
# Raised when an operation was attempted that is not compatible
|
|
1452
|
+
# with the protocol version being used.
|
|
1453
|
+
class VersionError < Exception
|
|
929
1454
|
end
|
|
930
1455
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
attr_reader :return_to
|
|
1456
|
+
# Raised when a response to a request cannot be generated
|
|
1457
|
+
# because the request contains no return_to URL.
|
|
1458
|
+
class NoReturnToError < Exception
|
|
1459
|
+
end
|
|
937
1460
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1461
|
+
# Could not encode this as a protocol message.
|
|
1462
|
+
#
|
|
1463
|
+
# You should probably render it and show it to the user.
|
|
1464
|
+
class EncodingError < Exception
|
|
1465
|
+
# The response that failed to encode.
|
|
1466
|
+
attr_reader :response
|
|
1467
|
+
|
|
1468
|
+
def initialize(response)
|
|
1469
|
+
super(response)
|
|
1470
|
+
@response = response
|
|
941
1471
|
end
|
|
942
|
-
|
|
943
1472
|
end
|
|
944
1473
|
|
|
945
|
-
|
|
1474
|
+
# This response is already signed.
|
|
1475
|
+
class AlreadySigned < EncodingError
|
|
1476
|
+
end
|
|
946
1477
|
|
|
1478
|
+
# A return_to is outside the trust_root.
|
|
947
1479
|
class UntrustedReturnURL < ProtocolError
|
|
948
|
-
|
|
949
1480
|
attr_reader :return_to, :trust_root
|
|
950
1481
|
|
|
951
|
-
def initialize(
|
|
952
|
-
super(
|
|
1482
|
+
def initialize(message, return_to, trust_root)
|
|
1483
|
+
super(message)
|
|
953
1484
|
@return_to = return_to
|
|
954
|
-
@trust_root = trust_root
|
|
1485
|
+
@trust_root = trust_root
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
def to_s
|
|
1489
|
+
return sprintf("return_to %s not under trust_root %s",
|
|
1490
|
+
@return_to,
|
|
1491
|
+
@trust_root)
|
|
955
1492
|
end
|
|
956
1493
|
end
|
|
957
1494
|
|
|
958
|
-
|
|
1495
|
+
# The return_to URL doesn't look like a valid URL.
|
|
1496
|
+
class MalformedReturnURL < ProtocolError
|
|
1497
|
+
attr_reader :return_to
|
|
959
1498
|
|
|
1499
|
+
def initialize(openid_message, return_to)
|
|
1500
|
+
@return_to = return_to
|
|
1501
|
+
super(openid_message)
|
|
1502
|
+
end
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
# The trust root is not well-formed.
|
|
1506
|
+
class MalformedTrustRoot < ProtocolError
|
|
1507
|
+
end
|
|
1508
|
+
end
|
|
960
1509
|
end
|