global_session 3.2.10 → 3.3.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 +4 -4
- data/global_session.gemspec +21 -86
- data/lib/global_session.rb +12 -7
- data/lib/global_session/directory.rb +8 -6
- data/lib/global_session/keystore.rb +28 -6
- data/lib/global_session/rack.rb +1 -1
- data/lib/global_session/session.rb +11 -6
- data/lib/global_session/session/abstract.rb +123 -4
- data/lib/global_session/session/v1.rb +6 -14
- data/lib/global_session/session/v2.rb +9 -17
- data/lib/global_session/session/v3.rb +11 -137
- data/lib/global_session/session/v4.rb +140 -0
- data/lib/global_session/version.rb +3 -0
- metadata +18 -91
- data/.ruby-version +0 -1
- data/.travis.yml +0 -11
- data/CHANGELOG.md +0 -94
- data/LICENSE +0 -20
- data/README.rdoc +0 -298
- data/Rakefile +0 -48
- data/VERSION +0 -1
- data/init.rb +0 -4
- data/rails/init.rb +0 -23
- data/rails_generators/global_session/USAGE +0 -1
- data/rails_generators/global_session/global_session_generator.rb +0 -51
- data/rails_generators/global_session/templates/global_session.yml.erb +0 -41
- data/rails_generators/global_session_authority/USAGE +0 -1
- data/rails_generators/global_session_authority/global_session_authority_generator.rb +0 -53
@@ -39,6 +39,9 @@ module GlobalSession::Session
|
|
39
39
|
# * The sign and verify algorithms, while safe, do not comply fully with PKCS7; they rely on the
|
40
40
|
# OpenSSL low-level crypto API instead of using the higher-level EVP (envelope) API.
|
41
41
|
class V1 < Abstract
|
42
|
+
# Pattern that matches strings that are probably a V1 session cookie.
|
43
|
+
HEADER = /^eN/
|
44
|
+
|
42
45
|
# Utility method to decode a cookie; good for console debugging. This performs no
|
43
46
|
# validation or security check of any sort.
|
44
47
|
#
|
@@ -233,15 +236,15 @@ module GlobalSession::Session
|
|
233
236
|
#Check signature
|
234
237
|
expected = canonical_digest(hash)
|
235
238
|
signer = @directory.authorities[authority]
|
236
|
-
raise
|
239
|
+
raise GlobalSession::InvalidSignature, "Unknown signing authority #{authority}" unless signer
|
237
240
|
got = signer.public_decrypt(GlobalSession::Encoding::Base64Cookie.load(signature))
|
238
241
|
unless (got == expected)
|
239
|
-
raise
|
242
|
+
raise GlobalSession::InvalidSignature, "Global session integrity failure; tampering suspected"
|
240
243
|
end
|
241
244
|
|
242
245
|
#Check trust in signing authority
|
243
246
|
unless @directory.trusted_authority?(authority)
|
244
|
-
raise
|
247
|
+
raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority} are not trusted"
|
245
248
|
end
|
246
249
|
|
247
250
|
#Check expiration
|
@@ -266,8 +269,6 @@ module GlobalSession::Session
|
|
266
269
|
end
|
267
270
|
|
268
271
|
def create_from_scratch # :nodoc:
|
269
|
-
authority_check
|
270
|
-
|
271
272
|
@signed = {}
|
272
273
|
@insecure = {}
|
273
274
|
@created_at = Time.now.utc
|
@@ -275,14 +276,5 @@ module GlobalSession::Session
|
|
275
276
|
@id = RightSupport::Data::UUID.generate
|
276
277
|
renew!
|
277
278
|
end
|
278
|
-
|
279
|
-
def create_invalid # :nodoc:
|
280
|
-
@id = nil
|
281
|
-
@created_at = Time.now.utc
|
282
|
-
@expired_at = created_at
|
283
|
-
@signed = {}
|
284
|
-
@insecure = {}
|
285
|
-
@authority = nil
|
286
|
-
end
|
287
279
|
end
|
288
280
|
end
|
@@ -44,6 +44,9 @@ module GlobalSession::Session
|
|
44
44
|
# * The sign and verify algorithms, while safe, do not comply fully with PKCS7; they rely on the
|
45
45
|
# OpenSSL low-level crypto API instead of using the higher-level EVP (envelope) API.
|
46
46
|
class V2 < Abstract
|
47
|
+
# Pattern that matches strings that are probably a V2 session cookie.
|
48
|
+
HEADER = /^l9/
|
49
|
+
|
47
50
|
# Utility method to decode a cookie; good for console debugging. This performs no
|
48
51
|
# validation or security check of any sort.
|
49
52
|
#
|
@@ -79,8 +82,8 @@ module GlobalSession::Session
|
|
79
82
|
hash['a'] = authority
|
80
83
|
signed_hash = RightSupport::Crypto::SignedHash.new(
|
81
84
|
hash.reject { |k,v| ['dx', 's'].include?(k) },
|
82
|
-
|
83
|
-
:
|
85
|
+
@directory.private_key,
|
86
|
+
encoding: GlobalSession::Encoding::Msgpack)
|
84
87
|
@signature = signed_hash.sign(@expired_at)
|
85
88
|
end
|
86
89
|
|
@@ -252,20 +255,20 @@ module GlobalSession::Session
|
|
252
255
|
|
253
256
|
#Check trust in signing authority
|
254
257
|
unless @directory.trusted_authority?(authority)
|
255
|
-
raise
|
258
|
+
raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
|
256
259
|
end
|
257
260
|
|
258
261
|
signed_hash = RightSupport::Crypto::SignedHash.new(
|
259
262
|
hash.reject { |k,v| ['dx', 's'].include?(k) },
|
260
|
-
|
261
|
-
:
|
263
|
+
@directory.authorities[authority],
|
264
|
+
:encoding=>GlobalSession::Encoding::Msgpack)
|
262
265
|
|
263
266
|
begin
|
264
267
|
signed_hash.verify!(signature, expired_at)
|
265
268
|
rescue RightSupport::Crypto::ExpiredSignature
|
266
269
|
raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
|
267
270
|
rescue RightSupport::Crypto::InvalidSignature => e
|
268
|
-
raise
|
271
|
+
raise GlobalSession::InvalidSignature, "Integrity check failed: " + e.message
|
269
272
|
end
|
270
273
|
|
271
274
|
#Check other validity (delegate to directory)
|
@@ -285,8 +288,6 @@ module GlobalSession::Session
|
|
285
288
|
end
|
286
289
|
|
287
290
|
def create_from_scratch # :nodoc:
|
288
|
-
authority_check
|
289
|
-
|
290
291
|
@signed = {}
|
291
292
|
@insecure = {}
|
292
293
|
@created_at = Time.now.utc
|
@@ -294,14 +295,5 @@ module GlobalSession::Session
|
|
294
295
|
@id = RightSupport::Data::UUID.generate
|
295
296
|
renew!
|
296
297
|
end
|
297
|
-
|
298
|
-
def create_invalid # :nodoc:
|
299
|
-
@id = nil
|
300
|
-
@created_at = Time.now.utc
|
301
|
-
@expired_at = created_at
|
302
|
-
@signed = {}
|
303
|
-
@insecure = {}
|
304
|
-
@authority = nil
|
305
|
-
end
|
306
298
|
end
|
307
299
|
end
|
@@ -22,7 +22,7 @@
|
|
22
22
|
# Standard library dependencies
|
23
23
|
require 'set'
|
24
24
|
|
25
|
-
#
|
25
|
+
# SignedHash, which encapsulates the crypto bit of global sessions
|
26
26
|
require 'right_support/crypto'
|
27
27
|
|
28
28
|
module GlobalSession::Session
|
@@ -47,6 +47,9 @@ module GlobalSession::Session
|
|
47
47
|
# encoding (instead of msgpack), and uses the undocumented OpenSSL::PKey#sign and #verify
|
48
48
|
# operations which rely on the PKCS7-compliant OpenSSL EVP API.
|
49
49
|
class V3 < Abstract
|
50
|
+
# Pattern that matches strings that are probably a V3 session cookie.
|
51
|
+
HEADER = /^WzM/
|
52
|
+
|
50
53
|
STRING_ENCODING = !!(RUBY_VERSION !~ /1.8/)
|
51
54
|
|
52
55
|
# Utility method to decode a cookie; good for console debugging. This performs no
|
@@ -110,33 +113,6 @@ module GlobalSession::Session
|
|
110
113
|
result
|
111
114
|
end
|
112
115
|
|
113
|
-
# Delete a key from the global session attributes. If the key exists,
|
114
|
-
# mark the global session dirty
|
115
|
-
#
|
116
|
-
# @param [String] the key to delete
|
117
|
-
# @return [Object] the value of the key deleted, or nil if not found
|
118
|
-
def delete(key)
|
119
|
-
key = key.to_s #take care of symbol-style keys
|
120
|
-
raise GlobalSession::InvalidSession unless valid?
|
121
|
-
|
122
|
-
if @schema_signed.include?(key)
|
123
|
-
authority_check
|
124
|
-
|
125
|
-
# Only mark dirty if the key actually exists
|
126
|
-
@dirty_secure = true if @signed.keys.include? key
|
127
|
-
value = @signed.delete(key)
|
128
|
-
elsif @schema_insecure.include?(key)
|
129
|
-
|
130
|
-
# Only mark dirty if the key actually exists
|
131
|
-
@dirty_insecure = true if @insecure.keys.include? key
|
132
|
-
value = @insecure.delete(key)
|
133
|
-
else
|
134
|
-
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
135
|
-
end
|
136
|
-
|
137
|
-
return value
|
138
|
-
end
|
139
|
-
|
140
116
|
# Serialize the session to a form suitable for use with HTTP cookies. If any
|
141
117
|
# secure attributes have changed since the session was instantiated, compute
|
142
118
|
# a fresh RSA signature.
|
@@ -168,9 +144,9 @@ module GlobalSession::Session
|
|
168
144
|
hash['a'] = authority
|
169
145
|
signed_hash = RightSupport::Crypto::SignedHash.new(
|
170
146
|
hash,
|
171
|
-
|
172
|
-
:
|
173
|
-
:
|
147
|
+
@directory.private_key,
|
148
|
+
envelope: true,
|
149
|
+
encoding: GlobalSession::Encoding::JSON)
|
174
150
|
@signature = signed_hash.sign(@expired_at)
|
175
151
|
end
|
176
152
|
|
@@ -183,87 +159,6 @@ module GlobalSession::Session
|
|
183
159
|
return GlobalSession::Encoding::Base64Cookie.dump(bin)
|
184
160
|
end
|
185
161
|
|
186
|
-
# Determine whether any state has changed since the session was loaded.
|
187
|
-
#
|
188
|
-
# @return [Boolean] true if something has changed
|
189
|
-
def dirty?
|
190
|
-
!!(super || @dirty_secure || @dirty_insecure)
|
191
|
-
end
|
192
|
-
|
193
|
-
# Return the keys that are currently present in the global session.
|
194
|
-
#
|
195
|
-
# === Return
|
196
|
-
# keys(Array):: List of keys contained in the global session
|
197
|
-
def keys
|
198
|
-
@signed.keys + @insecure.keys
|
199
|
-
end
|
200
|
-
|
201
|
-
# Return the values that are currently present in the global session.
|
202
|
-
#
|
203
|
-
# === Return
|
204
|
-
# values(Array):: List of values contained in the global session
|
205
|
-
def values
|
206
|
-
@signed.values + @insecure.values
|
207
|
-
end
|
208
|
-
|
209
|
-
# Iterate over each key/value pair
|
210
|
-
#
|
211
|
-
# === Block
|
212
|
-
# An iterator which will be called with each key/value pair
|
213
|
-
#
|
214
|
-
# === Return
|
215
|
-
# Returns the value of the last expression evaluated by the block
|
216
|
-
def each_pair(&block) # :yields: |key, value|
|
217
|
-
@signed.each_pair(&block)
|
218
|
-
@insecure.each_pair(&block)
|
219
|
-
end
|
220
|
-
|
221
|
-
# Lookup a value by its key.
|
222
|
-
#
|
223
|
-
# === Parameters
|
224
|
-
# key(String):: the key
|
225
|
-
#
|
226
|
-
# === Return
|
227
|
-
# value(Object):: The value associated with +key+, or nil if +key+ is not present
|
228
|
-
def [](key)
|
229
|
-
key = key.to_s #take care of symbol-style keys
|
230
|
-
@signed[key] || @insecure[key]
|
231
|
-
end
|
232
|
-
|
233
|
-
# Set a value in the global session hash. If the supplied key is denoted as
|
234
|
-
# secure by the global session schema, causes a new signature to be computed
|
235
|
-
# when the session is next serialized.
|
236
|
-
#
|
237
|
-
# === Parameters
|
238
|
-
# key(String):: The key to set
|
239
|
-
# value(Object):: The value to set
|
240
|
-
#
|
241
|
-
# === Return
|
242
|
-
# value(Object):: Always returns the value that was set
|
243
|
-
#
|
244
|
-
# ===Raise
|
245
|
-
# InvalidSession:: if the session has been invalidated (and therefore can't be written to)
|
246
|
-
# ArgumentError:: if the configuration doesn't define the specified key as part of the global session
|
247
|
-
# NoAuthority:: if the specified key is secure and the local node is not an authority
|
248
|
-
# UnserializableType:: if the specified value can't be serialized as JSON
|
249
|
-
def []=(key, value)
|
250
|
-
key = key.to_s #take care of symbol-style keys
|
251
|
-
raise GlobalSession::InvalidSession unless valid?
|
252
|
-
|
253
|
-
if @schema_signed.include?(key)
|
254
|
-
authority_check
|
255
|
-
@signed[key] = value
|
256
|
-
@dirty_secure = true
|
257
|
-
elsif @schema_insecure.include?(key)
|
258
|
-
@insecure[key] = value
|
259
|
-
@dirty_insecure = true
|
260
|
-
else
|
261
|
-
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
262
|
-
end
|
263
|
-
|
264
|
-
return value
|
265
|
-
end
|
266
|
-
|
267
162
|
# Return the SHA1 hash of the most recently-computed RSA signature of this session.
|
268
163
|
# This isn't really intended for the end user; it exists so the Web framework integration
|
269
164
|
# code can optimize request speed by caching the most recently verified signature in the
|
@@ -277,16 +172,6 @@ module GlobalSession::Session
|
|
277
172
|
|
278
173
|
private
|
279
174
|
|
280
|
-
# This is called by #clone and is used to augment the shallow clone behavior
|
281
|
-
#
|
282
|
-
# @return [Object] this global session object which doesn't reference the
|
283
|
-
# the hashes from the original object
|
284
|
-
def initialize_copy(source)
|
285
|
-
super
|
286
|
-
@signed = ::RightSupport::Data::HashTools.deep_clone2(@signed)
|
287
|
-
@insecure = ::RightSupport::Data::HashTools.deep_clone2(@insecure)
|
288
|
-
end
|
289
|
-
|
290
175
|
def load_from_cookie(cookie) # :nodoc:
|
291
176
|
hash = nil
|
292
177
|
|
@@ -311,20 +196,20 @@ module GlobalSession::Session
|
|
311
196
|
if @directory.trusted_authority?(authority)
|
312
197
|
signed_hash = RightSupport::Crypto::SignedHash.new(
|
313
198
|
hash,
|
199
|
+
@directory.authorities[authority],
|
314
200
|
:envelope=>true,
|
315
|
-
:encoding=>GlobalSession::Encoding::JSON
|
316
|
-
:public_key=>@directory.authorities[authority])
|
201
|
+
:encoding=>GlobalSession::Encoding::JSON)
|
317
202
|
|
318
203
|
begin
|
319
204
|
signed_hash.verify!(signature, expired_at)
|
320
205
|
rescue RightSupport::Crypto::ExpiredSignature
|
321
206
|
raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
|
322
207
|
rescue RightSupport::Crypto::InvalidSignature => e
|
323
|
-
raise
|
208
|
+
raise GlobalSession::InvalidSignature, "Global session signature verification failed: " + e.message
|
324
209
|
end
|
325
210
|
|
326
211
|
else
|
327
|
-
raise
|
212
|
+
raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
|
328
213
|
end
|
329
214
|
|
330
215
|
#Check expiration
|
@@ -349,8 +234,6 @@ module GlobalSession::Session
|
|
349
234
|
end
|
350
235
|
|
351
236
|
def create_from_scratch # :nodoc:
|
352
|
-
authority_check
|
353
|
-
|
354
237
|
@signed = {}
|
355
238
|
@insecure = {}
|
356
239
|
@created_at = Time.now.utc
|
@@ -359,15 +242,6 @@ module GlobalSession::Session
|
|
359
242
|
renew!
|
360
243
|
end
|
361
244
|
|
362
|
-
def create_invalid # :nodoc:
|
363
|
-
@id = nil
|
364
|
-
@created_at = Time.now.utc
|
365
|
-
@expired_at = created_at
|
366
|
-
@signed = {}
|
367
|
-
@insecure = {}
|
368
|
-
@authority = nil
|
369
|
-
end
|
370
|
-
|
371
245
|
# Transform a V1-style attribute hash to an Array with fixed placement for
|
372
246
|
# each element. The V3 scheme is serialized as an array to save space.
|
373
247
|
#
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module GlobalSession::Session
|
4
|
+
# Version 4 is based on JSON Web Token; in fact, if there is no insecure
|
5
|
+
# state, then a V4 session _is_ a JWT. Otherwise, it's a JWT with a
|
6
|
+
# nonstandard fourth component containing the insecure state.
|
7
|
+
class V4 < Abstract
|
8
|
+
EXPIRED_AT = 'exp'.freeze
|
9
|
+
ID = 'id'.freeze
|
10
|
+
ISSUED_AT = 'iat'.freeze
|
11
|
+
ISSUER = 'iss'.freeze
|
12
|
+
NOT_BEFORE = 'nbf'.freeze
|
13
|
+
|
14
|
+
# Pattern that matches strings that are probably a V4 session cookie.
|
15
|
+
HEADER = /^eyJ0eXAiOiJKV1QiL/
|
16
|
+
|
17
|
+
def self.decode_cookie(cookie)
|
18
|
+
header, payload, sig, insec = cookie.split('.')
|
19
|
+
header, payload, insec = [header, payload, insec].
|
20
|
+
map { |c| c && RightSupport::Data::Base64URL.decode(c) }.
|
21
|
+
map { |j| j && GlobalSession::Encoding::JSON.load(j) }
|
22
|
+
sig = RightSupport::Data::Base64URL.decode(sig)
|
23
|
+
insec ||= {}
|
24
|
+
|
25
|
+
unless Hash === header && header['typ'] == 'JWT'
|
26
|
+
raise GlobalSession::MalformedCookie, "JWT header not present"
|
27
|
+
end
|
28
|
+
unless Hash === payload
|
29
|
+
raise GlobalSession::MalformedCookie, "JWT payload not present"
|
30
|
+
end
|
31
|
+
|
32
|
+
[header, payload, sig, insec]
|
33
|
+
rescue JSON::ParserError => e
|
34
|
+
raise GlobalSession::MalformedCookie, e.message
|
35
|
+
end
|
36
|
+
|
37
|
+
# Serialize the session. If any secure attributes have changed since the
|
38
|
+
# session was instantiated, compute a fresh RSA signature.
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
def to_s
|
42
|
+
if @cookie && !dirty?
|
43
|
+
# nothing has changed; just return cached cookie
|
44
|
+
return @cookie
|
45
|
+
end
|
46
|
+
|
47
|
+
unless @insecure.nil? || @insecure.empty?
|
48
|
+
insec = GlobalSession::Encoding::JSON.dump(@insecure)
|
49
|
+
insec = RightSupport::Data::Base64URL.encode(insec)
|
50
|
+
end
|
51
|
+
|
52
|
+
if @signature && !(@dirty_timestamps || @dirty_secure)
|
53
|
+
# secure state hasn't changed; reuse JWT piece of cookie
|
54
|
+
jwt = @cookie.split('.')[0..2].join('.')
|
55
|
+
else
|
56
|
+
# secure state has changed; recompute signature & make new JWT
|
57
|
+
authority_check
|
58
|
+
|
59
|
+
payload = @signed.dup
|
60
|
+
payload[ID] = id
|
61
|
+
payload[EXPIRED_AT] = @expired_at.to_i
|
62
|
+
payload[ISSUED_AT] = @created_at.to_i
|
63
|
+
payload[ISSUER] = @directory.local_authority_name
|
64
|
+
|
65
|
+
sh = RightSupport::Crypto::SignedHash.new(payload, @directory.private_key, envelope: :jwt)
|
66
|
+
jwt = sh.to_jwt(@expired_at)
|
67
|
+
end
|
68
|
+
|
69
|
+
if insec && !insec.empty?
|
70
|
+
return "#{jwt}.#{insec}"
|
71
|
+
else
|
72
|
+
return jwt
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def load_from_cookie(cookie)
|
79
|
+
# Get the basic facts
|
80
|
+
header, payload, sig, insec = self.class.decode_cookie(cookie)
|
81
|
+
id = payload[ID]
|
82
|
+
created_at = payload[ISSUED_AT]
|
83
|
+
issuer = payload[ISSUER]
|
84
|
+
expired_at = payload[EXPIRED_AT]
|
85
|
+
not_before = payload[NOT_BEFORE]
|
86
|
+
raise GlobalSession::InvalidSignature, "JWT iat claim missing/wrong" unless Integer === created_at
|
87
|
+
raise GlobalSession::InvalidSignature, "JWT iat claim missing/wrong" unless Integer === expired_at
|
88
|
+
created_at = Time.at(created_at)
|
89
|
+
expired_at = Time.at(expired_at)
|
90
|
+
if Numeric === not_before
|
91
|
+
not_before = Time.at(not_before)
|
92
|
+
raise GlobalSession::PrematureSession, "Session not valid before #{not_before}" unless Time.now >= not_before
|
93
|
+
end
|
94
|
+
|
95
|
+
#Check trust in signing authority
|
96
|
+
if @directory.trusted_authority?(issuer)
|
97
|
+
signed_hash =
|
98
|
+
RightSupport::Crypto::SignedHash.new(payload,
|
99
|
+
@directory.authorities[issuer],
|
100
|
+
envelope: :jwt
|
101
|
+
)
|
102
|
+
|
103
|
+
begin
|
104
|
+
signed_hash.verify!(sig, expired_at)
|
105
|
+
rescue RightSupport::Crypto::ExpiredSignature
|
106
|
+
raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
|
107
|
+
rescue RightSupport::Crypto::InvalidSignature => e
|
108
|
+
raise GlobalSession::InvalidSignature, "Global session signature verification failed: " + e.message
|
109
|
+
end
|
110
|
+
|
111
|
+
else
|
112
|
+
raise GlobalSession::InvalidSignature, "Global sessions signed by #{authority.inspect} are not trusted"
|
113
|
+
end
|
114
|
+
|
115
|
+
#Check other validity (delegate to directory)
|
116
|
+
unless @directory.valid_session?(id, expired_at)
|
117
|
+
raise GlobalSession::InvalidSession, "Global session has been invalidated"
|
118
|
+
end
|
119
|
+
|
120
|
+
#If all validation stuff passed, assign our instance variables.
|
121
|
+
@id = id
|
122
|
+
@authority = issuer
|
123
|
+
@created_at = created_at
|
124
|
+
@expired_at = expired_at
|
125
|
+
@signed = payload
|
126
|
+
@insecure = insec
|
127
|
+
@signature = sig
|
128
|
+
@cookie = cookie
|
129
|
+
end
|
130
|
+
|
131
|
+
def create_from_scratch
|
132
|
+
@signed = {}
|
133
|
+
@insecure = {}
|
134
|
+
@created_at = Time.now.utc
|
135
|
+
@authority = @directory.local_authority_name
|
136
|
+
@id = generate_id
|
137
|
+
renew!
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|