ruby-openid2 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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