capability_tokens 0.2.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.
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +69 -0
- data/Rakefile +6 -0
- data/app/models/capability_tokens/capability_token.rb +13 -0
- data/capability_tokens.gemspec +27 -0
- data/config.ru +7 -0
- data/db/migrate/20140821110700_create_capability_tokens.rb +22 -0
- data/lib/capability_tokens.rb +44 -0
- data/lib/capability_tokens/configuration.rb +11 -0
- data/lib/capability_tokens/engine.rb +6 -0
- data/lib/capability_tokens/version.rb +3 -0
- data/spec/capability_token_spec.rb +35 -0
- data/spec/capability_tokens_spec.rb +50 -0
- data/spec/configuration_spec.rb +11 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/spec_helper.rb +15 -0
- metadata +183 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Robert Nubel
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# CapabilityTokens
|
2
|
+
|
3
|
+
[](https://test-travis-frontend.oak.enova.com/rnubel/capability_tokens)
|
4
|
+
|
5
|
+
This gem generates, stores, and helps you retrieve short-lived tokens with
|
6
|
+
payloads. It is intended to be part of a capability link system, which allows
|
7
|
+
users to perform actions without explicitly logging in.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'capability_tokens'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Then, install the migrations:
|
20
|
+
|
21
|
+
$ rake capability_tokens:install:migrations
|
22
|
+
|
23
|
+
And you're set! If you need to put the table this engine creates in a schema,
|
24
|
+
create an initializer:
|
25
|
+
|
26
|
+
# config/initializers/capability_tokens.rb
|
27
|
+
|
28
|
+
CapabilityTokens.configure do |c|
|
29
|
+
c.schema_name = 'my_stuff'
|
30
|
+
end
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Generate a new token:
|
35
|
+
|
36
|
+
requester = 'customer-service'
|
37
|
+
payload = { account_id: 1, action: 'login' }
|
38
|
+
cap_token = CapabilityTokens.generate(payload, requester, Time.now + 72.hours)
|
39
|
+
|
40
|
+
cap_token.token # => "82264468-6d50-454f-a257-007a89afa18b"
|
41
|
+
|
42
|
+
Disseminate the token as you see fit; e.g., in a link, like
|
43
|
+
`http://yourapp.com/do_it/82264468-6d50-454f-a257-007a89afa18b`.
|
44
|
+
|
45
|
+
When a user follows that link, your controller might do:
|
46
|
+
|
47
|
+
begin
|
48
|
+
token = CapabilityTokens.retrieve(params[:token])
|
49
|
+
login_user!(token.payload[:account_id])
|
50
|
+
rescue CapabilityTokens::ExpiredToken
|
51
|
+
raise "Too late!"
|
52
|
+
rescue CapabilityTokens::InvalidToken
|
53
|
+
raise "Hacker!"
|
54
|
+
end
|
55
|
+
|
56
|
+
Note that `CapabilityTokens::retrieve` will always raise an exception if the
|
57
|
+
retrieved token is either nonexistant or expired. You can rescue
|
58
|
+
`CapabilityTokens::BadToken` to catch all errors.
|
59
|
+
|
60
|
+
FYI, "requester" is required as a very basic audit trail. If your needs are
|
61
|
+
more complex, please open an issue and I'll investigate how to accomodate.
|
62
|
+
|
63
|
+
## Contributing
|
64
|
+
|
65
|
+
1. Fork it
|
66
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
67
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
68
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
69
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module CapabilityTokens
|
2
|
+
class CapabilityToken < ActiveRecord::Base
|
3
|
+
self.table_name = CapabilityTokens.configuration.table_name
|
4
|
+
|
5
|
+
serialize :payload
|
6
|
+
|
7
|
+
validates_presence_of :token, :expires_at, :payload, :requester
|
8
|
+
|
9
|
+
def expired?(as_of = Time.now)
|
10
|
+
expires_at <= as_of
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'capability_tokens/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "capability_tokens"
|
8
|
+
spec.version = CapabilityTokens::VERSION
|
9
|
+
spec.authors = ["Robert Nubel"]
|
10
|
+
spec.email = ["rnubel@enova.com"]
|
11
|
+
spec.description = %q{Generate and validate short-lived capability tokens.}
|
12
|
+
spec.summary = %q{Facilitates short-lived capability tokens for tasks like logging in without a password.}
|
13
|
+
spec.homepage = "http://www.enova.com"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.0'
|
24
|
+
spec.add_development_dependency 'rails', '~> 3.2'
|
25
|
+
spec.add_development_dependency 'sqlite3'
|
26
|
+
spec.add_development_dependency 'combustion', '~> 0.5.2'
|
27
|
+
end
|
data/config.ru
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class CreateCapabilityTokens < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table capability_tokens_table_name, primary_key: 'capability_token_id' do |t|
|
4
|
+
t.timestamps
|
5
|
+
t.string :token, null: false
|
6
|
+
t.string :requester, null: false
|
7
|
+
t.timestamp :expires_at, null: false
|
8
|
+
t.text :payload, null: false # Will serialize as YAML.
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index capability_tokens_table_name, :token, unique: true
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table capability_tokens_table_name
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def capability_tokens_table_name
|
20
|
+
CapabilityTokens.configuration.table_name
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "capability_tokens/version"
|
2
|
+
require "capability_tokens/configuration"
|
3
|
+
require "capability_tokens/engine"
|
4
|
+
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
module CapabilityTokens
|
8
|
+
BadToken = Class.new(StandardError)
|
9
|
+
ExpiredToken = Class.new(BadToken)
|
10
|
+
InvalidToken = Class.new(BadToken)
|
11
|
+
NonExpiredToken = Class.new(BadToken)
|
12
|
+
|
13
|
+
def self.configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield configuration
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.generate(payload, requester, expires_at = Time.now + 1.day)
|
22
|
+
token_model.create! payload: payload,
|
23
|
+
requester: requester,
|
24
|
+
expires_at: expires_at,
|
25
|
+
token: generate_token
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.retrieve(token_string, expired_requested = false)
|
29
|
+
token = token_model.where(token: token_string).first
|
30
|
+
raise InvalidToken if token.nil?
|
31
|
+
raise ExpiredToken if token.expired? && !expired_requested
|
32
|
+
raise NonExpiredToken if !token.expired? && expired_requested
|
33
|
+
token
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def self.token_model
|
38
|
+
CapabilityTokens::CapabilityToken
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.generate_token
|
42
|
+
SecureRandom.uuid
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CapabilityTokens::CapabilityToken do
|
4
|
+
let(:attributes) {
|
5
|
+
{ token: 'asdfaasdfafds',
|
6
|
+
requester: 'me',
|
7
|
+
expires_at: Time.now + 2.hours,
|
8
|
+
payload: { test: 'stuff' } }
|
9
|
+
}
|
10
|
+
|
11
|
+
it "can create" do
|
12
|
+
described_class.create! attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
it "requires a token, requester, payload, and expiration date" do
|
16
|
+
[:token, :requester, :payload, :expires_at].each do |missing_field|
|
17
|
+
expect {
|
18
|
+
described_class.create! attributes.except(missing_field)
|
19
|
+
}.to raise_error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can deserialize the payload" do
|
24
|
+
t = described_class.create! attributes
|
25
|
+
|
26
|
+
reloaded = described_class.find(t.id)
|
27
|
+
expect(reloaded.payload[:test]).to eql('stuff')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "enforces unique tokens" do
|
31
|
+
described_class.create! attributes
|
32
|
+
expect { described_class.create! attributes }.to raise_error
|
33
|
+
expect { described_class.create! attributes.merge(token: 'newtok') }.to_not raise_error
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CapabilityTokens do
|
4
|
+
let(:token) {
|
5
|
+
CapabilityTokens.generate({ account_id: 1 },
|
6
|
+
'random_person',
|
7
|
+
Time.now + 72.hours)
|
8
|
+
}
|
9
|
+
|
10
|
+
describe 'generating a new token through the API' do
|
11
|
+
it 'is persisted in the database' do
|
12
|
+
expect(token).to be_a CapabilityTokens::CapabilityToken
|
13
|
+
expect(token).to_not be_new_record
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'has a 32-byte GUID for its token' do
|
17
|
+
expect(token.token.gsub('-','').length).to eql(32)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'records the requester' do
|
21
|
+
expect(token.requester).to eql('random_person')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'retrieving a token' do
|
26
|
+
it 'can locate a token' do
|
27
|
+
expect(CapabilityTokens.retrieve(token.token)).to eql(token)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'raises CapabilityTokens::ExpiredToken if the token expired' do
|
31
|
+
token.update_column :expires_at, Time.now - 1.minute
|
32
|
+
|
33
|
+
expect {
|
34
|
+
CapabilityTokens.retrieve(token.token)
|
35
|
+
}.to raise_error CapabilityTokens::ExpiredToken
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises CapabilityTokens::InvalidToken if the token isn\'t found' do
|
39
|
+
expect {
|
40
|
+
CapabilityTokens.retrieve('notarealtoken')
|
41
|
+
}.to raise_error CapabilityTokens::InvalidToken
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'raises CapabilityTokens::NonExpiredToken if the token is not expired but requested expired token' do
|
45
|
+
expect {
|
46
|
+
CapabilityTokens.retrieve(token.token, true)
|
47
|
+
}.to raise_error CapabilityTokens::NonExpiredToken
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
File without changes
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: capability_tokens
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Nubel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-02-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec-rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rails
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.2'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.2'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sqlite3
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: combustion
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.5.2
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.5.2
|
110
|
+
description: Generate and validate short-lived capability tokens.
|
111
|
+
email:
|
112
|
+
- rnubel@enova.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- .gitignore
|
118
|
+
- .travis.yml
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- app/models/capability_tokens/capability_token.rb
|
124
|
+
- capability_tokens.gemspec
|
125
|
+
- config.ru
|
126
|
+
- db/migrate/20140821110700_create_capability_tokens.rb
|
127
|
+
- lib/capability_tokens.rb
|
128
|
+
- lib/capability_tokens/configuration.rb
|
129
|
+
- lib/capability_tokens/engine.rb
|
130
|
+
- lib/capability_tokens/version.rb
|
131
|
+
- spec/capability_token_spec.rb
|
132
|
+
- spec/capability_tokens_spec.rb
|
133
|
+
- spec/configuration_spec.rb
|
134
|
+
- spec/internal/config/database.yml
|
135
|
+
- spec/internal/config/routes.rb
|
136
|
+
- spec/internal/db/combustion_test.sqlite
|
137
|
+
- spec/internal/db/schema.rb
|
138
|
+
- spec/internal/log/.gitignore
|
139
|
+
- spec/internal/public/favicon.ico
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
homepage: http://www.enova.com
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
hash: -1568415840585632650
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
segments:
|
164
|
+
- 0
|
165
|
+
hash: -1568415840585632650
|
166
|
+
requirements: []
|
167
|
+
rubyforge_project:
|
168
|
+
rubygems_version: 1.8.23
|
169
|
+
signing_key:
|
170
|
+
specification_version: 3
|
171
|
+
summary: Facilitates short-lived capability tokens for tasks like logging in without
|
172
|
+
a password.
|
173
|
+
test_files:
|
174
|
+
- spec/capability_token_spec.rb
|
175
|
+
- spec/capability_tokens_spec.rb
|
176
|
+
- spec/configuration_spec.rb
|
177
|
+
- spec/internal/config/database.yml
|
178
|
+
- spec/internal/config/routes.rb
|
179
|
+
- spec/internal/db/combustion_test.sqlite
|
180
|
+
- spec/internal/db/schema.rb
|
181
|
+
- spec/internal/log/.gitignore
|
182
|
+
- spec/internal/public/favicon.ico
|
183
|
+
- spec/spec_helper.rb
|