mortal-token 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ec1263cb82fe020f0a5cb4ef18fc2b4fd29407f
4
+ data.tar.gz: c946ebf543665e2be2ab22f9f09d91f8147cf6d3
5
+ SHA512:
6
+ metadata.gz: 40703edd2fc20d6805d7d23f8dc45d06f16ebe89519a9c9520ad8446331a77d81df307c9a3a3d347fe0e75e37b6c38916790f6fff9bb57c146129f883f912c09
7
+ data.tar.gz: b5bee687a5f663783b952f0d4f522f938648afda737edd672abd6a15c9c22003f7b5d7e3a40a7585af95cbb63a738ff942fa478cc8d280d9fcf4c075a07f80b7
data/README.rdoc CHANGED
@@ -1,11 +1,10 @@
1
1
  == MortalToken, because some tokens shouldn't live forever
2
2
 
3
- MortalToken is a library for creating tokens that self-destruct after a specified time. No need to store and look up the
4
- token; it is self-verifying and self-expiring.
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
5
 
6
- The default lifespan is two of your Earth days. This does *not* mean "48 hours from when it was created". It means that
7
- the key will be valid throughout "today" and "tomorrow". If the lifespan were one day, a key created at 23:59 would expire
8
- in one minute at 00:00.
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.
9
8
 
10
9
  My original use case was login auth tokens for some simple Sinatra apps. I can also see it being useful for API auth.
11
10
  Of course there are potential uses outside of HTTP, too.
@@ -13,17 +12,16 @@ Of course there are potential uses outside of HTTP, too.
13
12
  Steps:
14
13
 
15
14
  1. Generate a new token
16
- 2. Give the client the resulting hash and salt
15
+ 2. Give the client the resulting digest, salt, and expiry timestamp
17
16
  3. Time passes
18
- 4. Receive a hash and salt from client
19
- 5. Reconstitute the token from the salt, and test if the hashes match. If not, the token has expired (or was forged).
20
-
21
- Read the full documentation at {jordanhollinger.com/docs/mortal-token/}[http://jordanhollinger.com/docs/mortal-token/].
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).
22
19
 
23
20
  == Warning
24
21
 
25
- This is, at best, alpha-quality software. It "works" in that it doesn't usually blow up, but it is not guaranteed to be "right".
26
- Don't use it in production, or at least not in any important production. May contain traces of peanut oil.
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.
27
25
 
28
26
  == Install
29
27
 
@@ -32,58 +30,72 @@ Don't use it in production, or at least not in any important production. May con
32
30
  Or add it to your Gemfile
33
31
  gem "mortal-token"
34
32
 
35
- == Example with Sinatra
33
+ == Example use with Sinatra
36
34
 
37
35
  require 'sinatra'
38
36
  require 'mortal-token'
39
37
 
40
- # You MUST set a secret key! Otherwise, anyone who looks at the source code will be able to forge tokens.
41
38
  MortalToken.secret = 'asdf092$78roasdjfjfaklmsdadASDFopijf98%2ejA#Df@sdf'
42
39
 
43
40
  post '/login' do
44
41
  if login_ok?
45
42
  token = MortalToken.new
46
- session[:token] = token.hash
43
+ # Or "token = MortalToken.new(current_user.id)" to set your own salt and verify who owns the session.
47
44
  session[:salt] = token.salt
45
+ session[:expires] = token.expires
46
+ session[:digest] = token.digest
48
47
  redirect '/secret'
49
48
  end
50
49
  end
51
50
 
52
51
  get '/secret' do
53
- token = MortalToken.new(session[:salt])
54
- if token == session[:token]
52
+ if MortalToken.check(session[:salt], session[:expires]).against(session[:digest])
55
53
  'Nice token!'
56
54
  else
57
55
  'Your token is expired or forged!'
58
56
  end
59
57
  end
60
58
 
61
- == Checking token validity
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
62
73
 
63
- A token's == and === methods accept another token or a hash. In the example above, an attempt has been made to reconstitute
64
- the original token (using it's salt) on the left. If successful, it will be able to regenerate the hash on the right. But if
65
- too much time has passed, the hashes will differ and we can conclude that the token has expired.
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.
66
78
 
67
79
  == Tweak token parameters
68
80
 
69
81
  You may tweak certain parameters of the library in order to make it more secure, less, faster, etc.
70
82
  These are the defaults (see the MortalToken class for documentation about each parameter):
71
83
 
72
- MortalToken.rounds = 5 # rounds of hashing
73
- MortalToken.units = :days # or :hours, :minutes
74
- MortalToken.valid_across = 2 # tokens are valid across N units
75
- MortalToken.max_salt_length = 50
76
- MortalToken.min_salt_length = 10
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
77
89
 
78
90
  == Multiple configurations
79
91
 
80
- Your application may want to use MortalTokens in various contexts, where the same parameters may not make sense (probably units and valid_across).
81
- You may define different scopes and give them each their own config. Always define the default scope first (as above). Other scopes will inherit
82
- its secret.
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.
83
95
 
84
96
  MortalToken.config(:foo) do |config|
85
97
  config.units = :minutes
86
- config.valid_across = 10
98
+ config.valid_for = 10
87
99
  end
88
100
 
89
101
  token = MortalToken.use(:foo).token
@@ -7,13 +7,12 @@ class MortalToken
7
7
  attr_reader :name
8
8
  # The master secret token (Keep it secret! Keep it safe!). Changing this will invalidate all existing tokens.
9
9
  attr_accessor :secret
10
- # The number of encryption rounds. Defaults to 5. Changing this will invalidate existing tokens.
11
- attr_accessor :rounds
12
- # The units that life is measured in - :days, :hours or :minutes
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
13
  attr_reader :units
14
- # The number of units tokens will be valid across. Defaults to 2, which (for :days) prevents a
15
- # token generated at 23:59 from expiring at 00:00. Changing this will invalidate existing tokens.
16
- attr_accessor :valid_across
14
+ # The number of units tokens will be valid across. Defaults to 1. Changing this will invalidate existing tokens.
15
+ attr_accessor :valid_for
17
16
  # The maximum salt length, defaults to 50
18
17
  attr_accessor :max_salt_length
19
18
  # The minimum salt length, defaults to 10
@@ -22,23 +21,27 @@ class MortalToken
22
21
  # Instantiates a new Configuration object default values
23
22
  def initialize(name)
24
23
  @name = name
25
- @rounds = 5
26
- @units = :days
27
- @valid_across = 2
24
+ @digest = 'sha256'
25
+ @units = :hours
26
+ @valid_for = 1
28
27
  @max_salt_length = 50
29
28
  @min_salt_length = 10
30
- # A temporary secret key. Use your own!!
31
- @secret = CONFIGS[:default] ? CONFIGS[:default].salt : self.salt
29
+ @secret = CONFIGS[:default].secret if CONFIGS[:default]
32
30
  end
33
31
 
34
32
  # Return a new token using this configuration
35
- def token(salt=nil, time=nil)
36
- Token.new(salt, time, self)
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)
37
40
  end
38
41
 
39
42
  # Set the units that life is measured in - :days, :hours or :minutes
40
43
  def units=(unit)
41
- raise ArgumentError, "MortalToken.units must be one of #{UNITS.keys.join(', ')}" unless UNITS.keys.include? unit
44
+ raise ArgumentError, "MortalToken.units must be one of #{Token::UNITS.keys.join(', ')}" unless Token::UNITS.keys.include? unit
42
45
  @units = unit
43
46
  end
44
47
 
@@ -48,5 +51,9 @@ class MortalToken
48
51
  pool_size = RAND_SEEDS.size
49
52
  (0..max_length).map { RAND_SEEDS[rand(pool_size)] }.join('')
50
53
  end
54
+
55
+ def _digest # :nodoc:
56
+ @_digest ||= OpenSSL::Digest.new(self.digest)
57
+ end
51
58
  end
52
59
  end
@@ -1,12 +1,11 @@
1
1
  # A token hash that "self-destructs" after a certain time.
2
2
  class MortalToken
3
3
  CONFIGS = {} # :nodoc:
4
- UNITS = {days: {increment: 1, format: '%Y-%m-%d'}, hours: {increment: 3600, format: '%Y-%m-%d_%H'}, minutes: {increment: 60, format: '%Y-%m-%d_%H:%M'}} # :nodoc:
5
4
 
6
5
  class << self
7
6
  # Returns a new Token. Also alised to #token.
8
- def new(salt=nil, time=nil)
9
- config.token(salt, time)
7
+ def new(salt=nil, expires=nil)
8
+ config.token(salt, expires)
10
9
  end
11
10
 
12
11
  # Returns a new or existing MortalToken::Configuration. If you pass a block, it will pass it the config object.
@@ -1,66 +1,70 @@
1
1
  class MortalToken
2
2
  class Token
3
- # The token's salt
3
+ UNITS = {days: {increment: 86400}, hours: {increment: 3600}, minutes: {increment: 60}} # :nodoc:
4
+
5
+ # The salt value
4
6
  attr_reader :salt
5
- attr_accessor :config
7
+ # The expiry time as a Unix timestamp
8
+ attr_reader :expires
9
+ attr_reader :config # :nodoc:
6
10
 
7
- # To create a brand new token, do *not* pass any arguments. To validate a hash from
8
- # an existing token, pass the existing token's salt.
9
- def initialize(salt=nil, start_time=nil, config=nil)
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)
10
14
  @config = config || MortalToken.config
11
- @salt = salt || config.salt
12
- @start_time = start_time || (config.units == :days ? Date.today : Time.now.utc)
15
+ @salt = salt || self.config.salt
16
+ @expires = expires ? expires.to_i : calculate_expiry
13
17
  end
14
18
 
15
- # Returns the hash value of the token
16
- def hash
17
- @hash ||= (0..config.rounds-1).inject("#{config.secret}_#{salt}_#{end_time.strftime(strfmt)}") do |val, i|
18
- Digest::SHA512.hexdigest(val)
19
- end
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}")
20
23
  end
21
24
 
22
- alias_method :to_s, :hash
25
+ # A URL-safe Base64-encoded digest
26
+ def urlsafe_digest
27
+ Base64.urlsafe_encode64(self.digest)
28
+ end
23
29
 
24
- # Tests this token against another token or token hash
25
- def ==(other_token_or_hash)
26
- each_time do |time|
27
- guess = config.token(salt, time)
28
- return true if guess.to_s == other_token_or_hash.to_s
29
- end
30
- return false
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
31
33
  end
32
34
 
33
- alias_method :===, :==
35
+ # Returns salt, expires, digest. Convenient for one-line assignment of all three.
36
+ def get
37
+ return salt, expires, digest
38
+ end
34
39
 
35
- private
40
+ alias_method :to_s, :digest
36
41
 
37
- # Iterates over each possible time unit and passes it to the block
38
- def each_time(&block)
39
- time = earliest_time
40
- while time <= end_time
41
- block.call(time)
42
- time += increment
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
43
50
  end
44
51
  end
45
52
 
46
- # Returns the end date/time of this token
47
- def end_time
48
- @start_time + (config.valid_across - 1) * increment
53
+ # Tests this token against another token or token hash. Even if it matches, returns false if
54
+ # the expire time is past.
55
+ 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
49
58
  end
50
59
 
51
- # Returns the earliest possible date/time this token could have been valid.
52
- def earliest_time
53
- @start_time - (config.valid_across - 1) * increment
54
- end
60
+ alias_method :===, :==
55
61
 
56
- # Returns the incrementor for the configured unit
57
- def increment
58
- UNITS[config.units][:increment]
59
- end
62
+ private
60
63
 
61
- # Returns the string formater for the configured unit
62
- def strfmt
63
- UNITS[config.units][:format]
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
64
68
  end
65
69
  end
66
70
  end
@@ -1,4 +1,4 @@
1
1
  class MortalToken
2
2
  # Library version
3
- VERSION = '0.1.0'
3
+ VERSION = '1.0.0'
4
4
  end
data/lib/mortal-token.rb CHANGED
@@ -1,6 +1,7 @@
1
- require 'date'
2
1
  require 'time'
3
- require 'digest/sha2'
2
+ require 'base64'
3
+ require 'openssl'
4
+
4
5
  require 'mortal-token/version'
5
6
  require 'mortal-token/token'
6
7
  require 'mortal-token/configuration'
metadata CHANGED
@@ -1,70 +1,50 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: mortal-token
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 1
8
- - 0
9
- version: 0.1.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
10
5
  platform: ruby
11
- authors:
6
+ authors:
12
7
  - Jordan Hollinger
13
8
  autorequire:
14
9
  bindir: bin
15
10
  cert_chain: []
16
-
17
- date: 2012-08-03 00:00:00 -04:00
18
- default_executable:
11
+ date: 2014-06-03 00:00:00.000000000 Z
19
12
  dependencies: []
20
-
21
- description: An EXPERIMENTAL library for generating self-contained, self-destructing tokens
13
+ description: An wrapper library for generating self-contained, self-destructing tokens
14
+ with HMAC
22
15
  email: jordan@jordanhollinger.com
23
16
  executables: []
24
-
25
17
  extensions: []
26
-
27
18
  extra_rdoc_files: []
28
-
29
- files:
19
+ files:
30
20
  - lib/mortal-token.rb
31
21
  - lib/mortal-token/mortal-token.rb
32
22
  - lib/mortal-token/token.rb
33
- - lib/mortal-token/version.rb
34
23
  - lib/mortal-token/configuration.rb
24
+ - lib/mortal-token/version.rb
35
25
  - README.rdoc
36
26
  - LICENSE
37
- has_rdoc: true
38
27
  homepage: http://github.com/jhollinger/mortal-token
39
28
  licenses: []
40
-
29
+ metadata: {}
41
30
  post_install_message:
42
31
  rdoc_options: []
43
-
44
- require_paths:
32
+ require_paths:
45
33
  - lib
46
- required_ruby_version: !ruby/object:Gem::Requirement
47
- none: false
48
- requirements:
49
- - - ">="
50
- - !ruby/object:Gem::Version
51
- segments:
52
- - 0
53
- version: "0"
54
- required_rubygems_version: !ruby/object:Gem::Requirement
55
- none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- segments:
60
- - 0
61
- version: "0"
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
62
44
  requirements: []
63
-
64
45
  rubyforge_project:
65
- rubygems_version: 1.3.7
46
+ rubygems_version: 2.0.14
66
47
  signing_key:
67
- specification_version: 3
68
- summary: Generate self-destructing tokens (experimental)
48
+ specification_version: 4
49
+ summary: Generate self-destructing tokens
69
50
  test_files: []
70
-