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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- 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
|