mortal-token 1.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6ec1263cb82fe020f0a5cb4ef18fc2b4fd29407f
4
- data.tar.gz: c946ebf543665e2be2ab22f9f09d91f8147cf6d3
3
+ metadata.gz: 6b8a22fd9bc7c9038590081f95128581bdab8d28
4
+ data.tar.gz: 6d0812debf82f95e6211377aa06467209480ded2
5
5
  SHA512:
6
- metadata.gz: 40703edd2fc20d6805d7d23f8dc45d06f16ebe89519a9c9520ad8446331a77d81df307c9a3a3d347fe0e75e37b6c38916790f6fff9bb57c146129f883f912c09
7
- data.tar.gz: b5bee687a5f663783b952f0d4f522f938648afda737edd672abd6a15c9c22003f7b5d7e3a40a7585af95cbb63a738ff942fa478cc8d280d9fcf4c075a07f80b7
6
+ metadata.gz: c7522365167e5e715a19a87c996e088c7cbcbee5f585a37a5a638167cb3f7a5fb263b97bc137d3f37f86dd73d5ebb9e26912644d357f6b3eb00fe158fb7572f0
7
+ data.tar.gz: 5e600b54b463f25eff00709b92b18652ecda7192bdb581ddb51c20a83d3c8e7a091f7f5bf36bb921bae91b8c10c2028d0686623da3140c3ac988a546ce090ea6
data/LICENSE CHANGED
@@ -1,13 +1,19 @@
1
- Copyright 2012 Jordan Hollinger
1
+ Copyright (c) 2015 Jordan Hollinger
2
2
 
3
- Licensed under the Apache License, Version 2.0 (the "License");
4
- you may not use this file except in compliance with the License.
5
- You may obtain a copy of the License at
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
6
9
 
7
- http://www.apache.org/licenses/LICENSE-2.0
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
8
12
 
9
- Unless required by applicable law or agreed to in writing, software
10
- distributed under the License is distributed on an "AS IS" BASIS,
11
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- See the License for the specific language governing permissions and
13
- limitations under the License.
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # MortalToken, because some tokens shouldn't live forever
2
+
3
+ MortalToken is a convenience wrapper for HMAC-based authentication. Tokens self-destruct after a specified time period; no need to store and look them up for verification. They may optionally contain a message, which is visible but tamper-proof.
4
+
5
+ ## Simple token with validity check
6
+
7
+ require 'mortal-token'
8
+ MortalToken.secret = 'asdf092$78roasdjfjfaklmsdadASDFopijf98%2ejA#Df@sdf'
9
+
10
+ token = MortalToken.create(60 * 60) # 1 hr
11
+ give_to_client token.to_s
12
+ token_str = get_from_client
13
+ if MortalToken.valid? token_str
14
+ # it's valid
15
+ else
16
+ # it's invalid or expired
17
+ end
18
+
19
+ ## Token with tamper-proof message
20
+
21
+ require 'mortal-token'
22
+ MortalToken.secret = 'asdf092$78roasdjfjfaklmsdadASDFopijf98%2ejA#Df@sdf'
23
+
24
+ token = MortalToken.create(60 * 60, 'some message')
25
+ give_to_client token.to_s
26
+ token_str = get_from_client
27
+ token, digest = MortalToken.recover(token_str)
28
+ if token == digest
29
+ # It's valid. But remember - the message could have been read by anyone with access to the token.
30
+ do_stuff_with token.message
31
+ else
32
+ # it's invalid or expired
33
+ end
34
+
35
+ ## Tweak token parameters
36
+
37
+ You may tweak certain parameters of the library in order to make it more secure, less, faster, etc. These are the defaults:
38
+
39
+ MortalToken.digest = 'sha512' # The digest algorithm used by HMAC
40
+ MortalToken.salt_length = 8 # Number of bits in tokens' salts
41
+
42
+ ## Testing
43
+
44
+ rake test
@@ -0,0 +1,18 @@
1
+ module MortalToken
2
+ class << self
3
+ # The master secret token (Keep it secret! Keep it safe!). Changing this will invalidate all existing tokens.
4
+ attr_accessor :secret
5
+ # The digest to use. Defaults to 'sha512'.
6
+ attr_reader :digest
7
+ # Salt length in bytes. Defaults to 8.
8
+ attr_accessor :salt_length
9
+ end
10
+
11
+ # Set a new digest type
12
+ def self.digest=(name)
13
+ @digest = OpenSSL::Digest.new name
14
+ end
15
+
16
+ self.digest = 'sha512'
17
+ self.salt_length = 8
18
+ end
@@ -1,26 +1,24 @@
1
- # A token hash that "self-destructs" after a certain time.
2
- class MortalToken
3
- CONFIGS = {} # :nodoc:
4
-
5
- class << self
6
- # Returns a new Token. Also alised to #token.
7
- def new(salt=nil, expires=nil)
8
- config.token(salt, expires)
9
- end
10
-
11
- # Returns a new or existing MortalToken::Configuration. If you pass a block, it will pass it the config object.
12
- # If it's a new config, it will inherit the default settings.
13
- def config(name=:default)
14
- config = (CONFIGS[name] ||= Configuration.new(name))
15
- yield config if block_given?
16
- config
17
- end
1
+ module MortalToken
2
+ # Create a new token that lasts for N seconds. Message is optional, but must be a string when present.
3
+ def self.create(seconds, message = nil)
4
+ expires = Time.now.utc.to_i + seconds
5
+ salt = SecureRandom.hex MortalToken.salt_length
6
+ Token.new expires, salt, message
7
+ end
18
8
 
19
- alias_method :use, :config
9
+ # Recover a token and digest created with MortalToken#to_s. Returns [token, digest].
10
+ # You must then check their validity with "token == digest"
11
+ def self.recover(token_str)
12
+ h = JSON.parse Base64.urlsafe_decode64 token_str.to_s
13
+ token = Token.new h['expires'], h['salt'], h['message']
14
+ return token, h['digest']
15
+ rescue ArgumentError, JSON::ParserError
16
+ nil
17
+ end
20
18
 
21
- # Alias all other methods to the default Configuration
22
- def method_missing(method, *args, &block)
23
- config.send(method, *args, &block)
24
- end
19
+ # Check if a token created with MoralToken#to_s is valid.
20
+ def self.valid?(token_str)
21
+ token, digest = recover token_str
22
+ token == digest
25
23
  end
26
24
  end
@@ -1,70 +1,70 @@
1
- class MortalToken
1
+ module MortalToken
2
+ # Create a token and check if it's still valid:
3
+ #
4
+ # token = MortalToken.create(300) # 5 min
5
+ # give_to_client token.to_s
6
+ # token_str = get_from_client
7
+ # MoralToken.valid? token_str
8
+ #
9
+ # Create a message token. The client *will* be able to read the message, but they *won't* be able to tamper with it.
10
+ # If your message must aslo be read-proof, you'll have to encrypt it and decrypt it yourself.
11
+ #
12
+ # token = MortalToken.create(300, "message")
13
+ # give_to_client token.to_s
14
+ # token_str = get_from_client
15
+ # token, digest = MortalToken.recover token_str
16
+ # if token == digest
17
+ # # It's valid
18
+ # do_stuff_with token.message
19
+ # else
20
+ # # The token was invalid or expired
21
+ # end
22
+ #
2
23
  class Token
3
- UNITS = {days: {increment: 86400}, hours: {increment: 3600}, minutes: {increment: 60}} # :nodoc:
4
-
5
- # The salt value
6
- attr_reader :salt
7
24
  # The expiry time as a Unix timestamp
8
25
  attr_reader :expires
9
- attr_reader :config # :nodoc:
10
-
11
- # To create a brand new token, do *not* pass any arguments. To validate a digest from
12
- # an existing token, pass the existing token's exiry timestamp.
13
- def initialize(salt=nil, expires=nil, config=nil)
14
- @config = config || MortalToken.config
15
- @salt = salt || self.config.salt
16
- @expires = expires ? expires.to_i : calculate_expiry
17
- end
18
-
19
- # Returns the hash digest of the token
20
- def digest
21
- raise "MortalToken: you must set a secret!" if config.secret.nil?
22
- @digest ||= OpenSSL::HMAC.digest(config._digest, config.secret, "#{expires.to_s}:#{salt}")
23
- end
26
+ # String content of token (optional)
27
+ attr_reader :message
28
+ # The salt value
29
+ attr_reader :salt
24
30
 
25
- # A URL-safe Base64-encoded digest
26
- def urlsafe_digest
27
- Base64.urlsafe_encode64(self.digest)
31
+ # Initialize an existing token
32
+ def initialize(expires, salt, message = nil)
33
+ @expires = expires.to_i
34
+ @salt = salt
35
+ @message = message ? message.to_s : nil
28
36
  end
29
37
 
30
- # Returns true if the token expires soon. Default check: within 5 min.
31
- def expires_soon?(min=5)
32
- Time.now.utc.to_i + (min * 60) >= expires
38
+ # Returns a URL-safe encoding of the token and its digest. Hand it out to users and check it with MoralToken.valid?
39
+ def to_s
40
+ h = to_h
41
+ h[:digest] = digest
42
+ Base64.urlsafe_encode64 h.to_json
33
43
  end
34
44
 
35
- # Returns salt, expires, digest. Convenient for one-line assignment of all three.
36
- def get
37
- return salt, expires, digest
45
+ # Returns HMAC hexdigest of the token
46
+ def digest
47
+ raise "MortalToken: you must set a secret!" if MortalToken.secret.nil?
48
+ @digest ||= OpenSSL::HMAC.hexdigest(MortalToken.digest, MortalToken.secret, to_h.to_json)
38
49
  end
39
50
 
40
- alias_method :to_s, :digest
41
-
42
- # Tests this token against another token or token hash. Accepts a block. If the check passes,
43
- # this token will be passed to the block.
44
- def against(other_token_or_digest)
45
- if self == other_token_or_digest
46
- yield self if block_given?
47
- true
48
- else
49
- false
50
- end
51
+ # Number of seconds remaining
52
+ def ttl
53
+ expires - Time.now.utc.to_i
51
54
  end
52
55
 
53
- # Tests this token against another token or token hash. Even if it matches, returns false if
54
- # the expire time is past.
56
+ # Tests this token against another token or token hash. Even if it matches, returns false if the expire time is past.
55
57
  def ==(other_token_or_digest)
56
- other = other_token_or_digest.to_s
57
- (self.digest == other || self.urlsafe_digest == other) && self.expires > Time.now.utc.to_i
58
+ other = other_token_or_digest.respond_to?(:digest) ? other_token_or_digest.digest : other_token_or_digest
59
+ self.digest == other && self.ttl > 0
58
60
  end
59
61
 
60
62
  alias_method :===, :==
61
63
 
62
64
  private
63
65
 
64
- # Returns the end date/time of this token
65
- def calculate_expiry
66
- expire_time = Time.now.utc.to_i + ((config.valid_for) * UNITS[config.units][:increment])
67
- expire_time.to_i
66
+ def to_h
67
+ {salt: salt, expires: expires, message: message}
68
68
  end
69
69
  end
70
70
  end
@@ -1,4 +1,4 @@
1
- class MortalToken
1
+ module MortalToken
2
2
  # Library version
3
- VERSION = '1.0.0'
3
+ VERSION = '2.0.0'
4
4
  end
data/lib/mortal-token.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'time'
2
+ require 'json'
2
3
  require 'base64'
3
4
  require 'openssl'
5
+ require 'securerandom'
4
6
 
7
+ require 'mortal-token/mortal-token'
5
8
  require 'mortal-token/version'
9
+ require 'mortal-token/config'
6
10
  require 'mortal-token/token'
7
- require 'mortal-token/configuration'
8
- require 'mortal-token/mortal-token'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mortal-token
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-03 00:00:00.000000000 Z
11
+ date: 2015-12-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: An wrapper library for generating self-contained, self-destructing tokens
14
14
  with HMAC
@@ -17,15 +17,16 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - LICENSE
21
+ - README.md
20
22
  - lib/mortal-token.rb
23
+ - lib/mortal-token/config.rb
21
24
  - lib/mortal-token/mortal-token.rb
22
25
  - lib/mortal-token/token.rb
23
- - lib/mortal-token/configuration.rb
24
26
  - lib/mortal-token/version.rb
25
- - README.rdoc
26
- - LICENSE
27
27
  homepage: http://github.com/jhollinger/mortal-token
28
- licenses: []
28
+ licenses:
29
+ - MIT
29
30
  metadata: {}
30
31
  post_install_message:
31
32
  rdoc_options: []
@@ -33,17 +34,17 @@ require_paths:
33
34
  - lib
34
35
  required_ruby_version: !ruby/object:Gem::Requirement
35
36
  requirements:
36
- - - '>='
37
+ - - ">="
37
38
  - !ruby/object:Gem::Version
38
- version: '0'
39
+ version: 2.0.0
39
40
  required_rubygems_version: !ruby/object:Gem::Requirement
40
41
  requirements:
41
- - - '>='
42
+ - - ">="
42
43
  - !ruby/object:Gem::Version
43
44
  version: '0'
44
45
  requirements: []
45
46
  rubyforge_project:
46
- rubygems_version: 2.0.14
47
+ rubygems_version: 2.4.5.1
47
48
  signing_key:
48
49
  specification_version: 4
49
50
  summary: Generate self-destructing tokens
data/README.rdoc DELETED
@@ -1,106 +0,0 @@
1
- == MortalToken, because some tokens shouldn't live forever
2
-
3
- MortalToken is a convenience wrapper for HMAC-based authentication. The tokens self-destruct after a specified time
4
- period; no need to store and look them up for verification.
5
-
6
- The default lifespan is one hour. For a Web-based application, you might extend this to many hours, or you
7
- might cut it back to only a few minutes, issuing a new token for each request/response cycle.
8
-
9
- My original use case was login auth tokens for some simple Sinatra apps. I can also see it being useful for API auth.
10
- Of course there are potential uses outside of HTTP, too.
11
-
12
- Steps:
13
-
14
- 1. Generate a new token
15
- 2. Give the client the resulting digest, salt, and expiry timestamp
16
- 3. Time passes
17
- 4. Receive a digest, salt, and expiry timestamp from client
18
- 5. Reconstitute the token from salt and timestamp, then see if the digests match. If not, the token has expired (or was forged).
19
-
20
- == Warning
21
-
22
- It is up to *you* to transmit the digest, salt, and expiry timestamp securely. If it's intercepted, someone could impersonate
23
- your client until the token expires. In other words, this is only *part* of an authentication solution. Use with caution. May
24
- contain traces of peanut.
25
-
26
- == Install
27
-
28
- [sudo] gem install mortal-token
29
-
30
- Or add it to your Gemfile
31
- gem "mortal-token"
32
-
33
- == Example use with Sinatra
34
-
35
- require 'sinatra'
36
- require 'mortal-token'
37
-
38
- MortalToken.secret = 'asdf092$78roasdjfjfaklmsdadASDFopijf98%2ejA#Df@sdf'
39
-
40
- post '/login' do
41
- if login_ok?
42
- token = MortalToken.new
43
- # Or "token = MortalToken.new(current_user.id)" to set your own salt and verify who owns the session.
44
- session[:salt] = token.salt
45
- session[:expires] = token.expires
46
- session[:digest] = token.digest
47
- redirect '/secret'
48
- end
49
- end
50
-
51
- get '/secret' do
52
- if MortalToken.check(session[:salt], session[:expires]).against(session[:digest])
53
- 'Nice token!'
54
- else
55
- 'Your token is expired or forged!'
56
- end
57
- end
58
-
59
- == Automatically re-issue nearly expired tokens
60
-
61
- MortalToken.check(session[:salt], session[:expires]).against(session[:digest]) do |token|
62
- session[:salt], session[:expires], session[:digest] = MortalToken.new.get if token.expires_soon?
63
- end
64
-
65
- == Checking token validity explained
66
-
67
- MortalToken.check(salt, expires).against(digest)
68
-
69
- is syntactic sugar for
70
-
71
- reconstituted_token = MortalToken.new(salt, expires)
72
- reconstituted_token == digest
73
-
74
- A token's == and === methods accept another token or a digest. In the example above, an attempt has been made to reconstitute
75
- the original token using it's salt and expiry timestamp. To be considered "equal", the reconstituted token's digest must
76
- match the original digest AND the timestamp must be in the future. Unless both of those conditions are met, then token is
77
- considered invalid or expired.
78
-
79
- == Tweak token parameters
80
-
81
- You may tweak certain parameters of the library in order to make it more secure, less, faster, etc.
82
- These are the defaults (see the MortalToken class for documentation about each parameter):
83
-
84
- MortalToken.valid_for = 1 # tokens are valid for N units
85
- MortalToken.units = :hours # or :days, :minutes
86
- MortalToken.digest = 'sha256' # The digest algorithm used by HMAC
87
- MortalToken.max_salt_length = 50 # Maximum token salt length
88
- MortalToken.min_salt_length = 10 # Minimum token salt length
89
-
90
- == Multiple configurations
91
-
92
- Your application may want to use MortalTokens in various contexts, where the same parameters may not make sense (probably units and valid_for).
93
- You may define different scopes and give them each their own config. Always define the default scope first (above). Other scopes will inherit
94
- its secret unless you specify another.
95
-
96
- MortalToken.config(:foo) do |config|
97
- config.units = :minutes
98
- config.valid_for = 10
99
- end
100
-
101
- token = MortalToken.use(:foo).token
102
-
103
- == License
104
- Copyright 2012 Jordan Hollinger
105
-
106
- Licensed under the Apache License
@@ -1,59 +0,0 @@
1
- class MortalToken
2
- # Holds a specific a configuration of parameters
3
- class Configuration
4
- RAND_SEEDS = [(0..9), ('a'..'z'), ('A'..'Z')].map(&:to_a).flatten # :nodoc;
5
-
6
- # The configuration's name
7
- attr_reader :name
8
- # The master secret token (Keep it secret! Keep it safe!). Changing this will invalidate all existing tokens.
9
- attr_accessor :secret
10
- # The digest to use (passed to OpenSSL::Digest.new). Defaults to 'sha256'.
11
- attr_accessor :digest
12
- # The units that life is measured in - :days, :hours or :minutes (defaults to :hours)
13
- attr_reader :units
14
- # The number of units tokens will be valid across. Defaults to 1. Changing this will invalidate existing tokens.
15
- attr_accessor :valid_for
16
- # The maximum salt length, defaults to 50
17
- attr_accessor :max_salt_length
18
- # The minimum salt length, defaults to 10
19
- attr_accessor :min_salt_length
20
-
21
- # Instantiates a new Configuration object default values
22
- def initialize(name)
23
- @name = name
24
- @digest = 'sha256'
25
- @units = :hours
26
- @valid_for = 1
27
- @max_salt_length = 50
28
- @min_salt_length = 10
29
- @secret = CONFIGS[:default].secret if CONFIGS[:default]
30
- end
31
-
32
- # Return a new token using this configuration
33
- def token(salt=nil, expires=nil)
34
- Token.new(salt, expires, self)
35
- end
36
-
37
- # Returns a token reconstitued from the timestamp and salt
38
- def check(salt, expires)
39
- Token.new(salt, expires, self)
40
- end
41
-
42
- # Set the units that life is measured in - :days, :hours or :minutes
43
- def units=(unit)
44
- raise ArgumentError, "MortalToken.units must be one of #{Token::UNITS.keys.join(', ')}" unless Token::UNITS.keys.include? unit
45
- @units = unit
46
- end
47
-
48
- # Returns a random string of between min_salt_length and max_salt_length alphanumeric charachters
49
- def salt
50
- max_length = [self.min_salt_length, rand(self.max_salt_length + 1)].max
51
- pool_size = RAND_SEEDS.size
52
- (0..max_length).map { RAND_SEEDS[rand(pool_size)] }.join('')
53
- end
54
-
55
- def _digest # :nodoc:
56
- @_digest ||= OpenSSL::Digest.new(self.digest)
57
- end
58
- end
59
- end