has_protected_token 0.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2402637577f83fe36f06f2f78c890acf609d2f88f119c19aa611c50b62e01883
4
+ data.tar.gz: 070f0809cc1e72075fc117fa108fbad54a96269efc621dacd61565a7b1ba9d87
5
+ SHA512:
6
+ metadata.gz: 9e4e4b5a199ace9e249752acedd415fa0082d70374971c0aaa6c4368c96db97c9bafe21837ee886d4eabec7db8a5cce398e99d5c7dba6829b9d0995471bec8dd
7
+ data.tar.gz: 52b14529a9d74b2cfa230149fcde14751d0560331d76b1b178bc17e8e1a3d452e040efc4a2805ab2ae48de8a00ba86957723f780d25b16cd88de52128523edab
@@ -0,0 +1,2 @@
1
+ .byebug_history
2
+ db/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1 @@
1
+ 2.5.7
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ before_install:
5
+ - gem install bundler -v 1.17.3
6
+
7
+ install:
8
+ - bundle install
9
+
10
+ branches:
11
+ only:
12
+ - master
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ has_protected_token (0.0.0.pre.alpha)
5
+ activerecord (>= 3.0)
6
+ bcrypt (~> 3.1.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (6.0.0)
12
+ activesupport (= 6.0.0)
13
+ activerecord (6.0.0)
14
+ activemodel (= 6.0.0)
15
+ activesupport (= 6.0.0)
16
+ activesupport (6.0.0)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.1, >= 2.1.8)
22
+ bcrypt (3.1.13)
23
+ bump (0.8.0)
24
+ byebug (11.0.1)
25
+ concurrent-ruby (1.1.5)
26
+ database_cleaner (1.7.0)
27
+ diff-lcs (1.3)
28
+ i18n (1.7.0)
29
+ concurrent-ruby (~> 1.0)
30
+ minitest (5.12.2)
31
+ rake (13.0.0)
32
+ rspec (3.8.0)
33
+ rspec-core (~> 3.8.0)
34
+ rspec-expectations (~> 3.8.0)
35
+ rspec-mocks (~> 3.8.0)
36
+ rspec-core (3.8.2)
37
+ rspec-support (~> 3.8.0)
38
+ rspec-expectations (3.8.5)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.8.0)
41
+ rspec-mocks (3.8.2)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.8.0)
44
+ rspec-support (3.8.3)
45
+ sqlite3 (1.4.1)
46
+ thread_safe (0.3.6)
47
+ tzinfo (1.2.5)
48
+ thread_safe (~> 0.1)
49
+ zeitwerk (2.2.0)
50
+
51
+ PLATFORMS
52
+ ruby
53
+
54
+ DEPENDENCIES
55
+ bump
56
+ bundler (~> 1.17.3)
57
+ byebug
58
+ database_cleaner
59
+ has_protected_token!
60
+ rake
61
+ rspec
62
+ sqlite3
63
+
64
+ BUNDLED WITH
65
+ 1.17.3
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019 David Allen
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,3 @@
1
+ # has_protected_token
2
+
3
+ This gem is a work in progress towards a minimum viable product. Version 0.1 should be released soon.
@@ -0,0 +1,10 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bump/tasks'
3
+
4
+ Bump.tag_by_default = true
5
+
6
+ RSpec::Core::RakeTask.new :test do |task|
7
+ task.pattern = Dir.glob('spec/**/*_spec.rb')
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'has_protected_token'
3
+ s.version = '0.0.0-alpha'
4
+ s.date = '2019-10-06'
5
+ s.summary = 'Easily generate random tokens for any ActiveRecord model and store them securely in the database.'
6
+ s.description = 'Generate random tokens (or use your own) for any ActiveRecord model. Hashes and salts the token before storage in the database using the same methodology as has_secure_password.'
7
+ s.author = 'David Allen'
8
+ s.email = '1337dallen@gmail.com' # yes, I know it's a terrible email address...
9
+ s.files = `git ls-files`.split("\n")
10
+ s.homepage = 'https://github.com/StaphSynth/has_protected_token'
11
+ s.license = 'MIT'
12
+
13
+ s.add_dependency 'activerecord', '>= 3.0'
14
+ s.add_dependency 'bcrypt', '~> 3.1.1'
15
+
16
+ s.add_development_dependency 'bundler', '~> 1.17.3'
17
+ s.add_development_dependency 'rake'
18
+ s.add_development_dependency 'rspec'
19
+ s.add_development_dependency 'sqlite3'
20
+ s.add_development_dependency 'byebug'
21
+ s.add_development_dependency 'database_cleaner'
22
+ s.add_development_dependency 'bump'
23
+ end
@@ -0,0 +1,116 @@
1
+ require 'active_record'
2
+ require 'bcrypt'
3
+
4
+ module ActiveRecord
5
+ module ProtectedToken
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # == has_protected_token
10
+ #
11
+ # Adds methods to set and validate against a token
12
+ # that has been hashed and salted using BCrypt.
13
+ # It assumes you have a 'token' attribute on
14
+ # your model.
15
+ #
16
+ # === Options
17
+ #
18
+ # +has_protected_token+ accepts an optional hash
19
+ # for modifying the following default behaviour:
20
+ #
21
+ # +column_name+
22
+ # If you would like to use an attribute other than
23
+ # 'token', pass +column_name: :my_attribute_name+
24
+ #
25
+ # +cost+
26
+ # BCrypt's default hashing cost is used. To use a
27
+ # different value, pass +cost: <value>+. The cost
28
+ # value must be an integer.
29
+ #
30
+ # === Example 1
31
+ #
32
+ # Generating a new token on the fly.
33
+ #
34
+ # class UserOne < ActiveRecord::Base
35
+ # has_protected_token column_name: :shared_secret, cost: 8
36
+ # end
37
+ #
38
+ # user1 = UserOne.new
39
+ # user1.regenerate_shared_secret
40
+ # => 'e13d0bbd4a12d2aea673127c7e995a67'
41
+ #
42
+ # user1.authenticate_shared_secret('not_even_close_to_correct')
43
+ # => false
44
+ #
45
+ # user1.authenticate_shared_secret('e13d0bbd4a12d2aea673127c7e995a67')
46
+ # => true
47
+ #
48
+ # === Example 2
49
+ #
50
+ # Passing your own token.
51
+ #
52
+ # class UserTwo < ActiveRecord::Base
53
+ # has_protected_token cost: 8
54
+ # end
55
+ #
56
+ # user2 = UserTwo.new
57
+ # user2.token = 'super_secret_token'
58
+ # => 'super_secret_token'
59
+ # user2.save!
60
+ # => true
61
+ # user2.token
62
+ # => '$2a$12$5xVuny6Z79bYfgMMU7nyzeaOSjygRnXfsJjeJHzRZ0vUYRGeUjo6u'
63
+ #
64
+ # user2.authenticate_token('totally_wrong')
65
+ # => false
66
+ #
67
+ # user2.authenticate_token('super_secret_token')
68
+ # => true
69
+ def has_protected_token(options = {})
70
+ attribute = options[:column_name] || :token
71
+ cost = options[:cost] || BCrypt::Engine::DEFAULT_COST
72
+
73
+ define_method("regenerate_#{attribute}") do
74
+ raw_token = self.class.generate_token
75
+ hashed_token = hash_token(raw_token, cost)
76
+
77
+ update_attribute(attribute, hashed_token)
78
+ raw_token
79
+ end
80
+
81
+ define_method("#{attribute}=") do |raw_token|
82
+ super(hash_token(raw_token, cost))
83
+ end
84
+
85
+ define_method("authenticate_#{attribute}") do |raw_token|
86
+ begin
87
+ BCrypt::Password.new(self.send(attribute)) == raw_token
88
+ rescue BCrypt::Error
89
+ false
90
+ end
91
+ end
92
+ end
93
+
94
+ # == .generate_token
95
+ # Class method to generate random tokens
96
+ #
97
+ # Accepts an optional integer to specify the length
98
+ # of the returned token.
99
+ def generate_token(length = 24)
100
+ n = length.to_i
101
+ SecureRandom.hex(n / 2) # hex returns n * 2
102
+
103
+ rescue NoMethodError
104
+ raise ArgumentError, 'Token length must be an integer'
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def hash_token(raw_token, cost)
111
+ BCrypt::Password.create(raw_token, :cost => cost)
112
+ end
113
+ end
114
+ end
115
+
116
+ ActiveRecord::Base.send(:include, ActiveRecord::ProtectedToken)
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::ProtectedToken do
4
+ let(:user) { User.create }
5
+ let(:raw_token) { 'raw_token' }
6
+ let(:hashed_token) { '$hashed_token$' }
7
+
8
+ describe 'options hash' do
9
+ describe 'column_name' do
10
+ context 'when no value is provided' do
11
+ it 'defaults to "token"' do
12
+ expect(user.respond_to?(:regenerate_token)).to be(true)
13
+ end
14
+ end
15
+
16
+ context 'when a symbol is passed' do
17
+ let(:user) { SpecialUser.create }
18
+
19
+ it 'uses it as an attribute name' do
20
+ expect(user.respond_to?(:regenerate_shared_secret)).to be(true)
21
+ expect(user.respond_to?(:regenerate_token)).to be(false)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe 'cost' do
27
+ context 'when no value provided' do
28
+ it 'defaults to BCrypt::Engine::DEFAULT_COST' do
29
+ expect(BCrypt::Password).to receive(:create).with(
30
+ raw_token,
31
+ cost: BCrypt::Engine::DEFAULT_COST
32
+ )
33
+
34
+ user.token = raw_token
35
+ end
36
+ end
37
+
38
+ context 'when an integer is provided' do
39
+ let(:user) { CostedUser.new }
40
+
41
+ it 'accepts that instead' do
42
+ expect(BCrypt::Password).to receive(:create).with(
43
+ raw_token,
44
+ cost: 9
45
+ )
46
+
47
+ user.token = raw_token
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'instance methods' do
54
+ before do
55
+ allow(User).to receive(:generate_token).and_return(raw_token)
56
+ allow(BCrypt::Password).to receive(:create).and_return(hashed_token)
57
+ allow(BCrypt::Password).to(
58
+ receive(:new).with(hashed_token).and_return(raw_token)
59
+ )
60
+ end
61
+
62
+ describe '#regenerate_{attribute}' do
63
+ it 'returns a new token' do
64
+ expect(user.regenerate_token).to eq(raw_token)
65
+ end
66
+
67
+ it 'hashes the new token and stores it in the database' do
68
+ user.regenerate_token
69
+
70
+ expect(user.reload.token).to eq(hashed_token)
71
+ end
72
+ end
73
+
74
+ describe '#authenticate_{attribute}' do
75
+ before do
76
+ user.regenerate_token
77
+ end
78
+
79
+ context 'when passed an plain text token' do
80
+ it 'returns true if it matches the stored value' do
81
+ expect(user.authenticate_token(raw_token)).to eq(true)
82
+ end
83
+
84
+ it 'returns false if it does not match the stored value' do
85
+ expect(user.authenticate_token('derp derp')).to eq(false)
86
+ end
87
+ end
88
+
89
+ context 'when passed bad data' do
90
+ before do
91
+ allow(BCrypt::Password).to receive(:new).and_raise(BCrypt::Error)
92
+ end
93
+
94
+ it 'returns false' do
95
+ expect(user.authenticate_token({ bad: 'data' })).to eq(false)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe '#{attribute}=' do
101
+ context 'when passed a value' do
102
+ it 'hashes it and stores the hashed value in the model instance' do
103
+ user.token = raw_token
104
+
105
+ expect(user.token).to eq(hashed_token)
106
+ expect(User.find(user.id).token).to be_nil
107
+ end
108
+
109
+ it 'returns the original value' do
110
+ expect(user.token = raw_token).to eq(raw_token)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe 'class methods' do
117
+ describe '.generate_token' do
118
+ let(:random_token) { 'abc123' }
119
+
120
+ context 'with no arguments' do
121
+ it 'returns a token 24 chars in length' do
122
+ expect(User.generate_token.size).to eq(24)
123
+ end
124
+ end
125
+
126
+ context 'when passing a length' do
127
+ it 'validates the length is coercable to an integer' do
128
+ expect{ User.generate_token(12) }.not_to raise_error
129
+ expect{ User.generate_token(false) }.to raise_error(ArgumentError)
130
+ end
131
+
132
+ it 'returns a token of that length' do
133
+ expect(User.generate_token(20).size).to eq(20)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,32 @@
1
+ require 'byebug'
2
+ require 'database_cleaner'
3
+ require 'has_protected_token'
4
+ require_relative './support/model'
5
+
6
+ RSpec.configure do |config|
7
+ config.shared_context_metadata_behavior = :apply_to_host_groups
8
+
9
+ config.expect_with :rspec do |expectations|
10
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
11
+ end
12
+
13
+ config.mock_with :rspec do |mocks|
14
+ mocks.verify_partial_doubles = true
15
+ end
16
+
17
+ config.before :suite do
18
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
19
+ ActiveRecord::Migration.suppress_messages do
20
+ load 'support/schema.rb'
21
+ end
22
+ end
23
+
24
+ config.before :each do
25
+ DatabaseCleaner.strategy = :transaction
26
+ DatabaseCleaner.start
27
+ end
28
+
29
+ config.after :each do
30
+ DatabaseCleaner.clean
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ class Model < ActiveRecord::Base; end
2
+
3
+ class User < Model
4
+ has_protected_token
5
+ end
6
+
7
+ class SpecialUser < Model
8
+ has_protected_token column_name: :shared_secret
9
+ end
10
+
11
+ class CostedUser < Model
12
+ has_protected_token cost: 9
13
+ end
@@ -0,0 +1,6 @@
1
+ ActiveRecord::Schema.define(version: 1) do
2
+ create_table :models do |t|
3
+ t.string :shared_secret
4
+ t.string :token
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_protected_token
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - David Allen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bcrypt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.1.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.1.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.17.3
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.17.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: database_cleaner
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bump
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Generate random tokens (or use your own) for any ActiveRecord model.
140
+ Hashes and salts the token before storage in the database using the same methodology
141
+ as has_secure_password.
142
+ email: 1337dallen@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".rspec"
149
+ - ".ruby-version"
150
+ - ".travis.yml"
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - LICENSE.txt
154
+ - README.md
155
+ - Rakefile
156
+ - has_protected_token.gemspec
157
+ - lib/has_protected_token.rb
158
+ - spec/lib/has_protected_token_spec.rb
159
+ - spec/spec_helper.rb
160
+ - spec/support/model.rb
161
+ - spec/support/schema.rb
162
+ homepage: https://github.com/StaphSynth/has_protected_token
163
+ licenses:
164
+ - MIT
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">"
178
+ - !ruby/object:Gem::Version
179
+ version: 1.3.1
180
+ requirements: []
181
+ rubyforge_project:
182
+ rubygems_version: 2.7.6.2
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: Easily generate random tokens for any ActiveRecord model and store them securely
186
+ in the database.
187
+ test_files: []