mortal-token 1.0.0 → 2.0.0

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