mortal-token 0.1.0 → 1.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
- data/README.rdoc +42 -30
- data/lib/mortal-token/configuration.rb +21 -14
- data/lib/mortal-token/mortal-token.rb +2 -3
- data/lib/mortal-token/token.rb +46 -42
- data/lib/mortal-token/version.rb +1 -1
- data/lib/mortal-token.rb +3 -2
- metadata +24 -44
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
|
4
|
-
|
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
|
7
|
-
|
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
|
15
|
+
2. Give the client the resulting digest, salt, and expiry timestamp
|
17
16
|
3. Time passes
|
18
|
-
4. Receive a
|
19
|
-
5. Reconstitute the token from
|
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
|
-
|
26
|
-
|
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
|
-
|
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
|
-
|
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
|
-
==
|
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
|
64
|
-
the original token
|
65
|
-
|
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.
|
73
|
-
MortalToken.units = :
|
74
|
-
MortalToken.
|
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
|
81
|
-
You may define different scopes and give them each their own config. Always define the default scope first (
|
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.
|
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
|
11
|
-
attr_accessor :
|
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
|
15
|
-
|
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
|
-
@
|
26
|
-
@units = :
|
27
|
-
@
|
24
|
+
@digest = 'sha256'
|
25
|
+
@units = :hours
|
26
|
+
@valid_for = 1
|
28
27
|
@max_salt_length = 50
|
29
28
|
@min_salt_length = 10
|
30
|
-
|
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,
|
36
|
-
Token.new(salt,
|
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,
|
9
|
-
config.token(salt,
|
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.
|
data/lib/mortal-token/token.rb
CHANGED
@@ -1,66 +1,70 @@
|
|
1
1
|
class MortalToken
|
2
2
|
class Token
|
3
|
-
|
3
|
+
UNITS = {days: {increment: 86400}, hours: {increment: 3600}, minutes: {increment: 60}} # :nodoc:
|
4
|
+
|
5
|
+
# The salt value
|
4
6
|
attr_reader :salt
|
5
|
-
|
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
|
8
|
-
# an existing token, pass the existing token's
|
9
|
-
def initialize(salt=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
|
-
@
|
15
|
+
@salt = salt || self.config.salt
|
16
|
+
@expires = expires ? expires.to_i : calculate_expiry
|
13
17
|
end
|
14
18
|
|
15
|
-
# Returns the hash
|
16
|
-
def
|
17
|
-
|
18
|
-
|
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
|
-
|
25
|
+
# A URL-safe Base64-encoded digest
|
26
|
+
def urlsafe_digest
|
27
|
+
Base64.urlsafe_encode64(self.digest)
|
28
|
+
end
|
23
29
|
|
24
|
-
#
|
25
|
-
def
|
26
|
-
|
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
|
-
|
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
|
-
|
40
|
+
alias_method :to_s, :digest
|
36
41
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
#
|
47
|
-
|
48
|
-
|
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
|
-
|
52
|
-
def earliest_time
|
53
|
-
@start_time - (config.valid_across - 1) * increment
|
54
|
-
end
|
60
|
+
alias_method :===, :==
|
55
61
|
|
56
|
-
|
57
|
-
def increment
|
58
|
-
UNITS[config.units][:increment]
|
59
|
-
end
|
62
|
+
private
|
60
63
|
|
61
|
-
# Returns the
|
62
|
-
def
|
63
|
-
UNITS[config.units][:
|
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
|
data/lib/mortal-token/version.rb
CHANGED
data/lib/mortal-token.rb
CHANGED
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
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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:
|
46
|
+
rubygems_version: 2.0.14
|
66
47
|
signing_key:
|
67
|
-
specification_version:
|
68
|
-
summary: Generate self-destructing tokens
|
48
|
+
specification_version: 4
|
49
|
+
summary: Generate self-destructing tokens
|
69
50
|
test_files: []
|
70
|
-
|