ruby-openid2 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +136 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/LICENSE.txt +210 -0
  7. data/README.md +81 -0
  8. data/SECURITY.md +15 -0
  9. data/lib/hmac/hmac.rb +110 -0
  10. data/lib/hmac/sha1.rb +11 -0
  11. data/lib/hmac/sha2.rb +25 -0
  12. data/lib/openid/association.rb +246 -0
  13. data/lib/openid/consumer/associationmanager.rb +354 -0
  14. data/lib/openid/consumer/checkid_request.rb +179 -0
  15. data/lib/openid/consumer/discovery.rb +516 -0
  16. data/lib/openid/consumer/discovery_manager.rb +144 -0
  17. data/lib/openid/consumer/html_parse.rb +142 -0
  18. data/lib/openid/consumer/idres.rb +513 -0
  19. data/lib/openid/consumer/responses.rb +147 -0
  20. data/lib/openid/consumer/session.rb +36 -0
  21. data/lib/openid/consumer.rb +406 -0
  22. data/lib/openid/cryptutil.rb +112 -0
  23. data/lib/openid/dh.rb +84 -0
  24. data/lib/openid/extension.rb +38 -0
  25. data/lib/openid/extensions/ax.rb +552 -0
  26. data/lib/openid/extensions/oauth.rb +88 -0
  27. data/lib/openid/extensions/pape.rb +170 -0
  28. data/lib/openid/extensions/sreg.rb +268 -0
  29. data/lib/openid/extensions/ui.rb +49 -0
  30. data/lib/openid/fetchers.rb +277 -0
  31. data/lib/openid/kvform.rb +113 -0
  32. data/lib/openid/kvpost.rb +62 -0
  33. data/lib/openid/message.rb +555 -0
  34. data/lib/openid/protocolerror.rb +7 -0
  35. data/lib/openid/server.rb +1571 -0
  36. data/lib/openid/store/filesystem.rb +260 -0
  37. data/lib/openid/store/interface.rb +73 -0
  38. data/lib/openid/store/memcache.rb +109 -0
  39. data/lib/openid/store/memory.rb +79 -0
  40. data/lib/openid/store/nonce.rb +72 -0
  41. data/lib/openid/trustroot.rb +597 -0
  42. data/lib/openid/urinorm.rb +72 -0
  43. data/lib/openid/util.rb +119 -0
  44. data/lib/openid/version.rb +5 -0
  45. data/lib/openid/yadis/accept.rb +141 -0
  46. data/lib/openid/yadis/constants.rb +16 -0
  47. data/lib/openid/yadis/discovery.rb +151 -0
  48. data/lib/openid/yadis/filters.rb +192 -0
  49. data/lib/openid/yadis/htmltokenizer.rb +290 -0
  50. data/lib/openid/yadis/parsehtml.rb +50 -0
  51. data/lib/openid/yadis/services.rb +44 -0
  52. data/lib/openid/yadis/xrds.rb +160 -0
  53. data/lib/openid/yadis/xri.rb +86 -0
  54. data/lib/openid/yadis/xrires.rb +87 -0
  55. data/lib/openid.rb +27 -0
  56. data/lib/ruby-openid.rb +1 -0
  57. data.tar.gz.sig +0 -0
  58. metadata +331 -0
  59. metadata.gz.sig +0 -0
@@ -0,0 +1,260 @@
1
+ # stdlib
2
+ require "fileutils"
3
+ require "pathname"
4
+ require "tempfile"
5
+
6
+ # This library
7
+ require_relative "../util"
8
+ require_relative "../association"
9
+ require_relative "interface"
10
+
11
+ module OpenID
12
+ module Store
13
+ class Filesystem < Interface
14
+ @@FILENAME_ALLOWED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-".split("")
15
+
16
+ # Create a Filesystem store instance, putting all data in +directory+.
17
+ def initialize(directory)
18
+ @nonce_dir = File.join(directory, "nonces")
19
+ @association_dir = File.join(directory, "associations")
20
+ @temp_dir = File.join(directory, "temp")
21
+
22
+ ensure_dir(@nonce_dir)
23
+ ensure_dir(@association_dir)
24
+ ensure_dir(@temp_dir)
25
+ end
26
+
27
+ # Create a unique filename for a given server url and handle. The
28
+ # filename that is returned will contain the domain name from the
29
+ # server URL for ease of human inspection of the data dir.
30
+ def get_association_filename(server_url, handle)
31
+ raise ArgumentError, "Bad server URL: #{server_url}" unless server_url.index("://")
32
+
33
+ proto, rest = server_url.split("://", 2)
34
+ domain = filename_escape(rest.split("/", 2)[0])
35
+ url_hash = safe64(server_url)
36
+ handle_hash = if handle
37
+ safe64(handle)
38
+ else
39
+ ""
40
+ end
41
+ filename = [proto, domain, url_hash, handle_hash].join("-")
42
+ File.join(@association_dir, filename)
43
+ end
44
+
45
+ # Store an association in the assoc directory
46
+ def store_association(server_url, association)
47
+ assoc_s = association.serialize
48
+ filename = get_association_filename(server_url, association.handle)
49
+ f, tmp = mktemp
50
+
51
+ begin
52
+ begin
53
+ f.write(assoc_s)
54
+ f.fsync
55
+ ensure
56
+ f.close
57
+ end
58
+
59
+ begin
60
+ File.rename(tmp, filename)
61
+ rescue Errno::EEXIST
62
+ begin
63
+ File.unlink(filename)
64
+ rescue Errno::ENOENT
65
+ # do nothing
66
+ end
67
+
68
+ File.rename(tmp, filename)
69
+ end
70
+ rescue StandardError
71
+ remove_if_present(tmp)
72
+ raise
73
+ end
74
+ end
75
+
76
+ # Retrieve an association
77
+ def get_association(server_url, handle = nil)
78
+ # the filename with empty handle is the prefix for the associations
79
+ # for a given server url
80
+ filename = get_association_filename(server_url, handle)
81
+ return _get_association(filename) if handle
82
+
83
+ assoc_filenames = Dir.glob(filename.to_s + "*")
84
+
85
+ assocs = assoc_filenames.collect do |f|
86
+ _get_association(f)
87
+ end
88
+
89
+ assocs = assocs.find_all { |a| !a.nil? }
90
+ assocs = assocs.sort_by { |a| a.issued }
91
+
92
+ return if assocs.empty?
93
+
94
+ assocs[-1]
95
+ end
96
+
97
+ def _get_association(filename)
98
+ assoc_file = File.open(filename, "r")
99
+ rescue Errno::ENOENT
100
+ nil
101
+ else
102
+ begin
103
+ assoc_s = assoc_file.read
104
+ ensure
105
+ assoc_file.close
106
+ end
107
+
108
+ begin
109
+ association = Association.deserialize(assoc_s)
110
+ rescue StandardError
111
+ remove_if_present(filename)
112
+ return
113
+ end
114
+
115
+ # clean up expired associations
116
+ return association unless association.expires_in == 0
117
+
118
+ remove_if_present(filename)
119
+ nil
120
+ end
121
+
122
+ # Remove an association if it exists, otherwise do nothing.
123
+ def remove_association(server_url, handle)
124
+ assoc = get_association(server_url, handle)
125
+
126
+ return false if assoc.nil?
127
+
128
+ filename = get_association_filename(server_url, handle)
129
+ remove_if_present(filename)
130
+ end
131
+
132
+ # Return whether the nonce is valid
133
+ def use_nonce(server_url, timestamp, salt)
134
+ return false if (timestamp - Time.now.to_i).abs > Nonce.skew
135
+
136
+ if server_url and !server_url.empty?
137
+ proto, rest = server_url.split("://", 2)
138
+ else
139
+ proto = ""
140
+ rest = ""
141
+ end
142
+ raise "Bad server URL" unless proto && rest
143
+
144
+ domain = filename_escape(rest.split("/", 2)[0])
145
+ url_hash = safe64(server_url)
146
+ salt_hash = safe64(salt)
147
+
148
+ nonce_fn = format("%08x-%s-%s-%s-%s", timestamp, proto, domain, url_hash, salt_hash)
149
+
150
+ filename = File.join(@nonce_dir, nonce_fn)
151
+
152
+ begin
153
+ fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0o200)
154
+ fd.close
155
+ true
156
+ rescue Errno::EEXIST
157
+ false
158
+ end
159
+ end
160
+
161
+ # Remove expired entries from the database. This is potentially expensive,
162
+ # so only run when it is acceptable to take time.
163
+ def cleanup
164
+ cleanup_associations
165
+ cleanup_nonces
166
+ end
167
+
168
+ def cleanup_associations
169
+ association_filenames = Dir[File.join(@association_dir, "*")]
170
+ count = 0
171
+ association_filenames.each do |af|
172
+ f = File.open(af, "r")
173
+ rescue Errno::ENOENT
174
+ next
175
+ else
176
+ begin
177
+ assoc_s = f.read
178
+ ensure
179
+ f.close
180
+ end
181
+ begin
182
+ association = OpenID::Association.deserialize(assoc_s)
183
+ rescue StandardError
184
+ remove_if_present(af)
185
+ next
186
+ else
187
+ if association.expires_in == 0
188
+ remove_if_present(af)
189
+ count += 1
190
+ end
191
+ end
192
+ end
193
+ count
194
+ end
195
+
196
+ def cleanup_nonces
197
+ nonces = Dir[File.join(@nonce_dir, "*")]
198
+ now = Time.now.to_i
199
+
200
+ count = 0
201
+ nonces.each do |filename|
202
+ nonce = filename.split("/")[-1]
203
+ timestamp = nonce.split("-", 2)[0].to_i(16)
204
+ nonce_age = (timestamp - now).abs
205
+ if nonce_age > Nonce.skew
206
+ remove_if_present(filename)
207
+ count += 1
208
+ end
209
+ end
210
+ count
211
+ end
212
+
213
+ protected
214
+
215
+ # Create a temporary file and return the File object and filename.
216
+ def mktemp
217
+ f = Tempfile.new("tmp", @temp_dir)
218
+ [f, f.path]
219
+ end
220
+
221
+ # create a safe filename from a url
222
+ def filename_escape(s)
223
+ s = "" if s.nil?
224
+ s.each_char.flat_map do |c|
225
+ if @@FILENAME_ALLOWED.include?(c)
226
+ c
227
+ else
228
+ c.bytes.map do |b|
229
+ "_%02X" % b
230
+ end
231
+ end
232
+ end.join
233
+ end
234
+
235
+ def safe64(s)
236
+ s = OpenID::CryptUtil.sha1(s)
237
+ s = OpenID::Util.to_base64(s)
238
+ s.tr!("+", "_")
239
+ s.tr!("/", ".")
240
+ s.delete!("=")
241
+ s
242
+ end
243
+
244
+ # remove file if present in filesystem
245
+ def remove_if_present(filename)
246
+ begin
247
+ File.unlink(filename)
248
+ rescue Errno::ENOENT
249
+ return false
250
+ end
251
+ true
252
+ end
253
+
254
+ # ensure that a path exists
255
+ def ensure_dir(dir_name)
256
+ FileUtils.mkdir_p(dir_name)
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,73 @@
1
+ require_relative "../util"
2
+
3
+ module OpenID
4
+ # Stores for Associations and nonces. Used by both the Consumer and
5
+ # the Server. If you have a database abstraction layer or other
6
+ # state storage in your application or framework already, you can
7
+ # implement the store interface.
8
+ module Store
9
+ # Abstract Store
10
+ # Changes in 2.0:
11
+ # * removed store_nonce, get_auth_key, is_dumb
12
+ # * changed use_nonce to support one-way nonces
13
+ # * added cleanup_nonces, cleanup_associations, cleanup
14
+ class Interface < Object
15
+ # Put a Association object into storage.
16
+ # When implementing a store, don't assume that there are any limitations
17
+ # on the character set of the server_url. In particular, expect to see
18
+ # unescaped non-url-safe characters in the server_url field.
19
+ def store_association(server_url, association)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # Returns a Association object from storage that matches
24
+ # the server_url. Returns nil if no such association is found or if
25
+ # the one matching association is expired. (Is allowed to GC expired
26
+ # associations when found.)
27
+ def get_association(server_url, handle = nil)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ # If there is a matching association, remove it from the store and
32
+ # return true, otherwise return false.
33
+ def remove_association(server_url, handle)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ # Return true if the nonce has not been used before, and store it
38
+ # for a while to make sure someone doesn't try to use the same value
39
+ # again. Return false if the nonce has already been used or if the
40
+ # timestamp is not current.
41
+ # You can use OpenID::Store::Nonce::SKEW for your timestamp window.
42
+ # server_url: URL of the server from which the nonce originated
43
+ # timestamp: time the nonce was created in seconds since unix epoch
44
+ # salt: A random string that makes two nonces issued by a server in
45
+ # the same second unique
46
+ def use_nonce(server_url, timestamp, salt)
47
+ raise NotImplementedError
48
+ end
49
+
50
+ # Remove expired nonces from the store
51
+ # Discards any nonce that is old enough that it wouldn't pass use_nonce
52
+ # Not called during normal library operation, this method is for store
53
+ # admins to keep their storage from filling up with expired data
54
+ def cleanup_nonces
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Remove expired associations from the store
59
+ # Not called during normal library operation, this method is for store
60
+ # admins to keep their storage from filling up with expired data
61
+ def cleanup_associations
62
+ raise NotImplementedError
63
+ end
64
+
65
+ # Remove expired nonces and associations from the store
66
+ # Not called during normal library operation, this method is for store
67
+ # admins to keep their storage from filling up with expired data
68
+ def cleanup
69
+ [cleanup_nonces, cleanup_associations]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,109 @@
1
+ # stdlib
2
+ require "time"
3
+
4
+ # This library
5
+ require_relative "../util"
6
+ require_relative "interface"
7
+ require_relative "nonce"
8
+
9
+ module OpenID
10
+ module Store
11
+ class Memcache < Interface
12
+ attr_accessor :key_prefix
13
+
14
+ def initialize(cache_client, key_prefix = "openid-store:")
15
+ @cache_client = cache_client
16
+ self.key_prefix = key_prefix
17
+ end
18
+
19
+ # Put a Association object into storage.
20
+ # When implementing a store, don't assume that there are any limitations
21
+ # on the character set of the server_url. In particular, expect to see
22
+ # unescaped non-url-safe characters in the server_url field.
23
+ def store_association(server_url, association)
24
+ serialized = serialize(association)
25
+ [nil, association.handle].each do |handle|
26
+ key = assoc_key(server_url, handle)
27
+ @cache_client.set(key, serialized, expiry(association.lifetime))
28
+ end
29
+ end
30
+
31
+ # Returns a Association object from storage that matches
32
+ # the server_url. Returns nil if no such association is found or if
33
+ # the one matching association is expired. (Is allowed to GC expired
34
+ # associations when found.)
35
+ def get_association(server_url, handle = nil)
36
+ serialized = @cache_client.get(assoc_key(server_url, handle))
37
+ return deserialize(serialized) if serialized
38
+
39
+ nil
40
+ end
41
+
42
+ # If there is a matching association, remove it from the store and
43
+ # return true, otherwise return false.
44
+ def remove_association(server_url, handle)
45
+ deleted = delete(assoc_key(server_url, handle))
46
+ server_assoc = get_association(server_url)
47
+ deleted = delete(assoc_key(server_url)) | deleted if server_assoc && server_assoc.handle == handle
48
+ deleted
49
+ end
50
+
51
+ # Return true if the nonce has not been used before, and store it
52
+ # for a while to make sure someone doesn't try to use the same value
53
+ # again. Return false if the nonce has already been used or if the
54
+ # timestamp is not current.
55
+ # You can use OpenID::Store::Nonce::SKEW for your timestamp window.
56
+ # server_url: URL of the server from which the nonce originated
57
+ # timestamp: time the nonce was created in seconds since unix epoch
58
+ # salt: A random string that makes two nonces issued by a server in
59
+ # the same second unique
60
+ def use_nonce(server_url, timestamp, salt)
61
+ return false if (timestamp - Time.now.to_i).abs > Nonce.skew
62
+
63
+ ts = timestamp.to_s # base 10 seconds since epoch
64
+ nonce_key = key_prefix + "N" + server_url + "|" + ts + "|" + salt
65
+ result = @cache_client.add(nonce_key, "", expiry(Nonce.skew + 5))
66
+ return !!(result =~ /^STORED/) if result.is_a?(String)
67
+
68
+ !!result
69
+ end
70
+
71
+ def assoc_key(server_url, assoc_handle = nil)
72
+ key = key_prefix + "A" + server_url
73
+ key += "|" + assoc_handle if assoc_handle
74
+ key
75
+ end
76
+
77
+ def cleanup_nonces
78
+ end
79
+
80
+ def cleanup
81
+ end
82
+
83
+ def cleanup_associations
84
+ end
85
+
86
+ protected
87
+
88
+ def delete(key)
89
+ result = @cache_client.delete(key)
90
+ return !!(result =~ /^DELETED/) if result.is_a?(String)
91
+
92
+ !!result
93
+ end
94
+
95
+ def serialize(assoc)
96
+ Marshal.dump(assoc)
97
+ end
98
+
99
+ def deserialize(assoc_str)
100
+ Marshal.load(assoc_str)
101
+ end
102
+
103
+ # Convert a lifetime in seconds into a memcache expiry value
104
+ def expiry(t)
105
+ Time.now.to_i + t
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,79 @@
1
+ require_relative "interface"
2
+
3
+ module OpenID
4
+ module Store
5
+ # An in-memory implementation of Store. This class is mainly used
6
+ # for testing, though it may be useful for long-running single
7
+ # process apps. Note that this store is NOT thread-safe.
8
+ #
9
+ # You should probably be looking at OpenID::Store::Filesystem
10
+ class Memory < Interface
11
+ def initialize
12
+ @associations = Hash.new { |hash, key| hash[key] = {} }
13
+ @nonces = {}
14
+ end
15
+
16
+ def store_association(server_url, assoc)
17
+ assocs = @associations[server_url]
18
+ @associations[server_url] = assocs.merge({assoc.handle => deepcopy(assoc)})
19
+ end
20
+
21
+ def get_association(server_url, handle = nil)
22
+ assocs = @associations[server_url]
23
+ if handle
24
+ assocs[handle]
25
+ else
26
+ assocs.values.sort_by(&:issued)[-1]
27
+ end
28
+ end
29
+
30
+ def remove_association(server_url, handle)
31
+ assocs = @associations[server_url]
32
+ return true if assocs.delete(handle)
33
+
34
+ false
35
+ end
36
+
37
+ def use_nonce(server_url, timestamp, salt)
38
+ return false if (timestamp - Time.now.to_i).abs > Nonce.skew
39
+
40
+ nonce = [server_url, timestamp, salt].join("")
41
+ return false if @nonces[nonce]
42
+
43
+ @nonces[nonce] = timestamp
44
+ true
45
+ end
46
+
47
+ def cleanup_associations
48
+ count = 0
49
+ @associations.each do |_server_url, assocs|
50
+ assocs.each do |handle, assoc|
51
+ if assoc.expires_in == 0
52
+ assocs.delete(handle)
53
+ count += 1
54
+ end
55
+ end
56
+ end
57
+ count
58
+ end
59
+
60
+ def cleanup_nonces
61
+ count = 0
62
+ now = Time.now.to_i
63
+ @nonces.each do |nonce, timestamp|
64
+ if (timestamp - now).abs > Nonce.skew
65
+ @nonces.delete(nonce)
66
+ count += 1
67
+ end
68
+ end
69
+ count
70
+ end
71
+
72
+ protected
73
+
74
+ def deepcopy(o)
75
+ Marshal.load(Marshal.dump(o))
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,72 @@
1
+ # stdlib
2
+ require "date"
3
+ require "time"
4
+
5
+ # This library
6
+ require_relative "../cryptutil"
7
+
8
+ module OpenID
9
+ module Nonce
10
+ DEFAULT_SKEW = 60 * 60 * 5
11
+ TIME_FMT = "%Y-%m-%dT%H:%M:%SZ"
12
+ TIME_STR_LEN = "0000-00-00T00:00:00Z".size
13
+ @@NONCE_CHRS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
14
+ TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/
15
+
16
+ @skew = DEFAULT_SKEW
17
+
18
+ # The allowed nonce time skew in seconds. Defaults to 5 hours.
19
+ # Used for checking nonce validity, and by stores' cleanup methods.
20
+ def self.skew
21
+ @skew
22
+ end
23
+
24
+ def self.skew=(new_skew)
25
+ @skew = new_skew
26
+ end
27
+
28
+ # Extract timestamp from a nonce string
29
+ def self.split_nonce(nonce_str)
30
+ timestamp_str = nonce_str[0...TIME_STR_LEN]
31
+ raise ArgumentError if timestamp_str.size < TIME_STR_LEN
32
+ raise ArgumentError unless timestamp_str.match(TIME_VALIDATOR)
33
+
34
+ ts = Time.parse(timestamp_str).to_i
35
+ raise ArgumentError if ts < 0
36
+
37
+ [ts, nonce_str[TIME_STR_LEN..-1]]
38
+ end
39
+
40
+ # Is the timestamp that is part of the specified nonce string
41
+ # within the allowed clock-skew of the current time?
42
+ def self.check_timestamp(nonce_str, allowed_skew = nil, now = nil)
43
+ allowed_skew = skew if allowed_skew.nil?
44
+ begin
45
+ stamp, = split_nonce(nonce_str)
46
+ rescue ArgumentError # bad timestamp
47
+ return false
48
+ end
49
+ now ||= Time.now.to_i
50
+
51
+ # times before this are too old
52
+ past = now - allowed_skew
53
+
54
+ # times newer than this are too far in the future
55
+ future = now + allowed_skew
56
+
57
+ (past <= stamp and stamp <= future)
58
+ end
59
+
60
+ # generate a nonce with the specified timestamp (defaults to now)
61
+ def self.mk_nonce(time = nil)
62
+ salt = CryptUtil.random_string(6, @@NONCE_CHRS)
63
+ t = if time.nil?
64
+ Time.now.getutc
65
+ else
66
+ Time.at(time).getutc
67
+ end
68
+ time_str = t.strftime(TIME_FMT)
69
+ time_str + salt
70
+ end
71
+ end
72
+ end