capability_tokens 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ cache: bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in capability_tokens.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,69 @@
1
+ # CapabilityTokens
2
+
3
+ [![Build Status](https://test-travis-frontend.oak.enova.com/rnubel/capability_tokens.png?token=6cmRdX1SLVTFvWBgBbCZ&branch=master)](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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :all
7
+ run Combustion::Application
@@ -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,11 @@
1
+ module CapabilityTokens
2
+ class Configuration < Struct.new(:schema_name)
3
+ def initialize
4
+ self.schema_name = nil
5
+ end
6
+
7
+ def table_name
8
+ schema_name ? "#{schema_name}.capability_tokens" : "capability_tokens"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module CapabilityTokens
2
+ class Engine < Rails::Engine
3
+ engine_name 'capability_tokens'
4
+ end
5
+ end
6
+
@@ -0,0 +1,3 @@
1
+ module CapabilityTokens
2
+ VERSION = "0.2.0"
3
+ 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
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe CapabilityTokens::Configuration do
4
+ describe '#table_name' do
5
+ it 'respects schema_name' do
6
+ subject.schema_name = 'foo'
7
+
8
+ expect(subject.table_name).to eql 'foo.capability_tokens'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ #
3
+ end
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Schema.define do
2
+ #
3
+ end
@@ -0,0 +1 @@
1
+ *.log
File without changes
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'combustion'
5
+
6
+ Combustion.initialize! :all
7
+
8
+ require 'rspec/rails'
9
+ require 'capability_tokens'
10
+
11
+ RSpec.configure do |config|
12
+ config.use_transactional_fixtures = true
13
+ end
14
+
15
+
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