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 +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
|
-
|