tiny_auth 0.1.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a857ca2f5a5570a3c6eeeb44c38f181b6e65cd5a
4
- data.tar.gz: ce6eda4d395ec74c98837fe4ebb92fb03be5e6aa
2
+ SHA256:
3
+ metadata.gz: 44afb63af8c4c286b6ea58e6cea65629621614931f630f76ef0fdac12b73f8fb
4
+ data.tar.gz: 64331660202e7610cb8d459c8cf6e21a73f9e0665d02e197c79a89c8e9b49a8a
5
5
  SHA512:
6
- metadata.gz: 421a42289951a95406cb3b06e29339e5e9d35bb08d09a2933f112b110df28647c0cc6b139b6f934f62a7173cb56f545bb0e063ae92efbd19c6814ea43ff59472
7
- data.tar.gz: 72e2a5ab7f10ef4ab1db43ca70b3ae56e0bf6ed4515277362f644ab538935937ea8d37ca22ffc62e0fe676f1170956e2340d860d47e2406297cadab0149f68b9
6
+ metadata.gz: 629a9db85d09fa28e1b92891a272bed123f26b2c1d8f6a889547a5751fd53eb03b7729eeb7ba37e2d2e499798ae748c0d65bb73abb88ee4d06c32b91f3109115
7
+ data.tar.gz: 5ae7c9d1e667d321298cebc4b5df889ce472c7b5d6293e66f8cf40552236cb506db41226ef22beb5fa250eeb34be59f2088099cd0b7de3a55cab36319f5f114a
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ /Gemfile.lock
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in tiny_auth.gemspec
4
4
  gemspec
5
+
6
+ gem "simplecov"
7
+ gem "coveralls"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # TinyAuth
1
+ # TinyAuth [![Build Status](https://travis-ci.org/rzane/tiny_auth.svg?branch=master)](https://travis-ci.org/rzane/tiny_auth) [![Coverage Status](https://coveralls.io/repos/github/rzane/tiny_auth/badge.svg?branch=master)](https://coveralls.io/github/rzane/tiny_auth?branch=master)
2
2
 
3
3
  A utility for minimal user authentication.
4
4
 
@@ -16,16 +16,17 @@ And then execute:
16
16
 
17
17
  ## Usage
18
18
 
19
+ ### `TinyAuth::Model`
20
+
19
21
  First, create a table to store your users:
20
22
 
21
23
  ```ruby
22
24
  create_table :users do |t|
23
25
  t.string :email, null: false
24
26
  t.string :password_digest, null: false
25
- t.string :reset_token
26
- t.datetime :reset_token_expires_at
27
+ t.integer :token_version, null: false, default: 0
27
28
  t.index :email, unique: true
28
- t.index :reset_token, unique: true
29
+ t.index [:id, :token_version], unique: true
29
30
  end
30
31
  ```
31
32
 
@@ -33,34 +34,99 @@ Your model should look like this:
33
34
 
34
35
  ```ruby
35
36
  class User < ApplicationRecord
36
- has_secure_password
37
+ include TinyAuth::Model
37
38
  end
38
39
  ```
39
40
 
40
- Now, you're ready to use `TinyAuth`:
41
+ #### `#generate_token(purpose: :access, expires_in: 24.hours)`
42
+
43
+ Generate a token. The token is generated from the user's `id` and their `token_version`.
44
+
45
+ If the `token_version` changes, all previously issued tokens will be revoked. Anytime the
46
+ user's password changes, this will happen automatically.
47
+
48
+ ```ruby
49
+ irb> user.generate_token
50
+ "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJ..."
51
+
52
+ irb> user.generate_token(purpose: :reset, expires_in: 1.hour)
53
+ "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJ..."
54
+ ```
55
+
56
+ #### `#invalidate_tokens`
57
+
58
+ Increments the `#token_version`, but does not apply the change to the database.
59
+
60
+ #### `#invalidate_tokens!`
61
+
62
+ Increments the `#token_version` and applies the change to the database.
63
+
64
+ #### `.find_by_email(email)`
65
+
66
+ Find a user by their email address. The query will disregard casing.
67
+
68
+ ```ruby
69
+ irb> User.find_by_email("user@example.com")
70
+ #<User id: 1, email: "user@example.com">
71
+ ```
72
+
73
+ #### `.find_by_credentials(email, password)`
74
+
75
+ Find a user by their email, then check that the password matches.
76
+
77
+ If the email doesn't exist, `nil` will be returned. If the password doesn't match, `nil` will be returned.
41
78
 
42
79
  ```ruby
43
- auth = TinyAuth.new(User)
80
+ irb> User.find_by_credentials("user@example.com", "testing123")
81
+ #<User id: 1, email: "user@example.com">
82
+
83
+ irb> User.find_by_credentials("user@example.com", "")
84
+ nil
85
+
86
+ irb> User.find_by_credentials("", "")
87
+ nil
88
+ ```
44
89
 
45
- user = auth.find_by_email('user@example.com')
46
- user = auth.find_by_credentials('user@example.com', 'password')
90
+ #### `.find_by_token(token, purpose: :access)`
47
91
 
48
- token = auth.generate_token(user)
49
- user = auth.find_by_token(token)
92
+ Find a user by their token. If the user can't be found, `nil` will be returned.
50
93
 
51
- reset_token = auth.generate_reset_token(user)
52
- user = auth.exchange_reset_token(user, password: "changed")
94
+ ```ruby
95
+ irb> User.find_by_token(token)
96
+ #<User id: 1, email: "user@example.com">
97
+
98
+ irb> User.find_by_token(reset_token, purpose: :reset)
99
+ #<User id: 1, email: "user@example.com">
100
+
101
+ irb> User.find_by_token("")
102
+ nil
53
103
  ```
54
104
 
55
- ## Development
105
+ ### `TinyAuth::Controller`
106
+
107
+ ```ruby
108
+ class ApplicationController < ActionController::Base
109
+ include TinyAuth::Controller.new(model: User)
110
+ end
111
+ ```
112
+
113
+ The example above would generate the following methods based on the model's name:
114
+
115
+ #### `#authenticate_user`
116
+
117
+ This method should be called in a `before_action`. If an `Authorization` header is found, it will attempt to locate a user.
118
+
119
+ #### `#current_user`
120
+
121
+ An accessor that can be used to obtain access to the authenticated user after calling `authenticate_user`.
56
122
 
57
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+ #### `#user_signed_in?`
58
124
 
59
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
125
+ A convenience method to determine if a user is signed in.
60
126
 
61
127
  ## Contributing
62
128
 
63
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tiny_auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
129
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rzane/tiny_auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
64
130
 
65
131
  ## License
66
132
 
@@ -68,4 +134,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
68
134
 
69
135
  ## Code of Conduct
70
136
 
71
- Everyone interacting in the TinyAuth project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/tiny_auth/blob/master/CODE_OF_CONDUCT.md).
137
+ Everyone interacting in the TinyAuth project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rzane/tiny_auth/blob/master/CODE_OF_CONDUCT.md).
data/bin/console CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
+ require "irb"
5
+ require "active_record"
4
6
  require "tiny_auth"
5
7
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
+ require_relative "../spec/support/schema"
9
+ require_relative "../spec/support/models"
8
10
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
11
+ TinyAuth.secret = "supersecret"
12
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
13
+
14
+ User.create!(email: "user@example.com", password: "testing123")
12
15
 
13
- require "irb"
14
16
  IRB.start(__FILE__)
data/docs/UPGRADING.md ADDED
@@ -0,0 +1,17 @@
1
+ # Upgrading
2
+
3
+ ## 2.x to 3.x
4
+
5
+ - Change all occurences of `generate_reset_token` to use `generate_token`.
6
+ - Change all occurences of `exchange_reset_token` to use `find_by_token`.
7
+ - Add a new migration:
8
+
9
+ ```ruby
10
+ change_table :users do |t|
11
+ t.remove :reset_token_digest
12
+ t.remove :reset_token_expires_at
13
+
14
+ t.integer :token_version, null: false, default: 0
15
+ t.index [:id, :token_version], unique: true
16
+ end
17
+ ```
data/lib/tiny_auth.rb CHANGED
@@ -1,61 +1,25 @@
1
+ require "tiny_auth/controller"
2
+ require "tiny_auth/model"
3
+ require "tiny_auth/verifier"
1
4
  require "tiny_auth/version"
2
- require "globalid"
3
- require "active_record"
4
- require "active_support/core_ext/securerandom"
5
5
 
6
- class TinyAuth
7
- class Error < StandardError; end
8
- class PersistError < StandardError; end
9
-
10
- def initialize(model, scope: model)
11
- @model = model
12
- @scope = scope
13
- end
14
-
15
- def find_by_email(email)
16
- @scope.find_by(@model.arel_table[:email].lower.eq(email.downcase))
17
- end
18
-
19
- def find_by_credentials(email, password)
20
- resource = find_by_email(email)
21
- resource if resource&.authenticate(password)
22
- end
23
-
24
- def generate_token(resource, purpose: :access, expires_in: 24.hours)
25
- resource.to_sgid(expires_in: expires_in, for: purpose).to_s
26
- end
27
-
28
- def find_by_token(token, purpose: :access)
29
- GlobalID::Locator.locate_signed(token, for: purpose)
30
- rescue ActiveRecord::RecordNotFound
31
- end
32
-
33
- def generate_reset_token(resource, expires_in: 2.hours)
34
- update_reset(
35
- resource,
36
- reset_token: SecureRandom.base58(24),
37
- reset_token_expires_at: Time.now + expires_in
38
- )
39
-
40
- resource.reset_token
41
- end
42
-
43
- def exchange_reset_token(reset_token, changes = {})
44
- changes = changes.merge(reset_token: nil, reset_token_expires_at: nil)
45
- not_expired = @model.arel_table[:reset_token_expires_at].gt(Time.now)
46
- resource = @scope.where(not_expired).find_by(reset_token: reset_token)
47
-
48
- yield resource if resource && block_given?
49
- update_reset(resource, changes) if resource
50
- end
51
-
52
- private
6
+ module TinyAuth
7
+ class << self
8
+ # Configure the secret used to sign and verify tokens.
9
+ # @param secret [String]
10
+ def secret=(secret)
11
+ @verifier = Verifier.new(secret)
12
+ end
53
13
 
54
- def update_reset(resource, changes)
55
- if resource.update(changes)
56
- resource
57
- else
58
- raise PersistError, "Failed to reset password."
14
+ def verifier # :nodoc:
15
+ @verifier || raise("Secret has not been configured")
59
16
  end
60
17
  end
61
18
  end
19
+
20
+ begin
21
+ require "rails/railtie"
22
+ rescue LoadError
23
+ else
24
+ require "tiny_auth/railtie"
25
+ end
@@ -0,0 +1,55 @@
1
+ require "active_support/core_ext/object/blank"
2
+
3
+ module TinyAuth
4
+ class Controller < Module
5
+ # Extract a token from a request
6
+ # @param request [ActionDispatch::HTTP::Request]
7
+ # @return [String,nil]
8
+ def self.token(request)
9
+ header = request.authorization.to_s
10
+ header[/^Bearer (.*)$/, 1].presence
11
+ end
12
+
13
+ # Defines a before action that will authenticate the resource.
14
+ # It also defines methods for accessing the currently authenticated
15
+ # resource.
16
+ # @param model [ActiveRecord::Base]
17
+ # @param name [Symbol] Used to define methods like `current_user`
18
+ #
19
+ # @example
20
+ # class ApplicationController < ActionController::Base
21
+ # include TinyAuth::Controller.new(model: User)
22
+ #
23
+ # before_action :authenticate_user
24
+ #
25
+ # def index
26
+ # if user_signed_in?
27
+ # render json: current_user
28
+ # else
29
+ # head :unauthorized
30
+ # end
31
+ # end
32
+ # end
33
+ def initialize(model:, name: model.model_name.singular)
34
+ authenticate = :"authenticate_#{name}"
35
+ current = :"current_#{name}"
36
+ current_ivar = :"@current_#{name}"
37
+ signed_in = :"#{name}_signed_in?"
38
+
39
+ attr_reader current
40
+
41
+ define_method(signed_in) do
42
+ !send(current).nil?
43
+ end
44
+
45
+ define_method(authenticate) do
46
+ token = TinyAuth::Controller.token(request)
47
+
48
+ if token
49
+ resource = model.find_by_token(token)
50
+ instance_variable_set(current_ivar, resource)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ require "active_record"
2
+ require "active_support/core_ext/numeric/time"
3
+
4
+ module TinyAuth
5
+ module Model
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.has_secure_password
9
+ base.before_save :invalidate_tokens, if: :password_digest_changed?
10
+ end
11
+
12
+ module ClassMethods
13
+ # Find a resource by email, ignoring case
14
+ # @param email [String]
15
+ # @return [ActiveRecord::Base,nil]
16
+ def find_by_email(email)
17
+ find_by(arel_table[:email].lower.eq(email.downcase))
18
+ end
19
+
20
+ # Find a resource by their email address and password
21
+ # This assumes that you've added `has_secure_password` to your model.
22
+ # @param email [String]
23
+ # @param password [String]
24
+ # @return [ActiveRecord::Base,nil]
25
+ def find_by_credentials(email, password)
26
+ resource = find_by_email(email)
27
+ resource if resource&.authenticate(password)
28
+ end
29
+
30
+ # Finds a resource by a token
31
+ # @param token [String]
32
+ # @param purpose [Symbol] defaults to `:access`
33
+ # @return [ActiveRecord::Base,nil]
34
+ def find_by_token(token, purpose: :access)
35
+ id, token_version = TinyAuth.verifier.verify(token, purpose: purpose)
36
+ find_by(id: id, token_version: token_version)
37
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
38
+ end
39
+ end
40
+
41
+ # Generates a token for this resource.
42
+ # @param expires_in [ActiveSupport::Duration] defaults to 24 hours
43
+ # @param purpose [Symbol] defaults to `:access`
44
+ # @return [String]
45
+ def generate_token(purpose: :access, expires_in: 24.hours)
46
+ TinyAuth.verifier.generate(
47
+ [id, token_version],
48
+ purpose: purpose,
49
+ expires_in: expires_in
50
+ )
51
+ end
52
+
53
+ # Invalidate all tokens for this resource. The token version will
54
+ # be incremented and written to the database.
55
+ # @return [self]
56
+ def invalidate_tokens!
57
+ increment!(:token_version)
58
+ end
59
+
60
+ # Invalidate all tokens for this resource. The token version will
61
+ # be incremented, but it will not be written to the database.
62
+ def invalidate_tokens
63
+ increment(:token_version)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ module TinyAuth
2
+ class Railtie < Rails::Railtie # :nodoc:
3
+ initializer "tiny_auth" do |app|
4
+ TinyAuth.secret = app.key_generator.generate_key("tiny_auth")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ require "base64"
2
+ require "active_support/message_verifier"
3
+
4
+ module TinyAuth
5
+ class Verifier < ActiveSupport::MessageVerifier # :nodoc:
6
+ private
7
+
8
+ def encode(data)
9
+ ::Base64.urlsafe_encode64(data)
10
+ end
11
+
12
+ def decode(data)
13
+ ::Base64.urlsafe_decode64(data)
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
- class TinyAuth
2
- VERSION = "0.1.2"
1
+ module TinyAuth
2
+ VERSION = "3.0.0"
3
3
  end
data/tiny_auth.gemspec CHANGED
@@ -26,7 +26,6 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.add_dependency "activerecord", "~> 6.0"
28
28
  spec.add_dependency "activesupport", "~> 6.0"
29
- spec.add_dependency "globalid", "~> 0.4"
30
29
 
31
30
  spec.add_development_dependency "bundler", "~> 2.0"
32
31
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tiny_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Zane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-15 00:00:00.000000000 Z
11
+ date: 2021-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
- - !ruby/object:Gem::Dependency
42
- name: globalid
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '0.4'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '0.4'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: bundler
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -134,13 +120,16 @@ files:
134
120
  - ".travis.yml"
135
121
  - CODE_OF_CONDUCT.md
136
122
  - Gemfile
137
- - Gemfile.lock
138
123
  - LICENSE.txt
139
124
  - README.md
140
125
  - Rakefile
141
126
  - bin/console
142
- - bin/setup
127
+ - docs/UPGRADING.md
143
128
  - lib/tiny_auth.rb
129
+ - lib/tiny_auth/controller.rb
130
+ - lib/tiny_auth/model.rb
131
+ - lib/tiny_auth/railtie.rb
132
+ - lib/tiny_auth/verifier.rb
144
133
  - lib/tiny_auth/version.rb
145
134
  - tiny_auth.gemspec
146
135
  homepage: https://github.com/rzane/tiny_auth
@@ -149,7 +138,7 @@ licenses:
149
138
  metadata:
150
139
  homepage_uri: https://github.com/rzane/tiny_auth
151
140
  source_code_uri: https://github.com/rzane/tiny_auth
152
- post_install_message:
141
+ post_install_message:
153
142
  rdoc_options: []
154
143
  require_paths:
155
144
  - lib
@@ -164,9 +153,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
153
  - !ruby/object:Gem::Version
165
154
  version: '0'
166
155
  requirements: []
167
- rubyforge_project:
168
- rubygems_version: 2.6.14
169
- signing_key:
156
+ rubygems_version: 3.2.3
157
+ signing_key:
170
158
  specification_version: 4
171
159
  summary: Bare-minimum authentication for APIs
172
160
  test_files: []
data/Gemfile.lock DELETED
@@ -1,63 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- tiny_auth (0.1.1)
5
- activerecord (~> 6.0)
6
- activesupport (~> 6.0)
7
- globalid (~> 0.4)
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- activemodel (6.0.1)
13
- activesupport (= 6.0.1)
14
- activerecord (6.0.1)
15
- activemodel (= 6.0.1)
16
- activesupport (= 6.0.1)
17
- activesupport (6.0.1)
18
- concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (>= 0.7, < 2)
20
- minitest (~> 5.1)
21
- tzinfo (~> 1.1)
22
- zeitwerk (~> 2.2)
23
- bcrypt (3.1.13)
24
- concurrent-ruby (1.1.5)
25
- diff-lcs (1.3)
26
- globalid (0.4.2)
27
- activesupport (>= 4.2.0)
28
- i18n (1.7.0)
29
- concurrent-ruby (~> 1.0)
30
- minitest (5.13.0)
31
- rake (10.5.0)
32
- rspec (3.9.0)
33
- rspec-core (~> 3.9.0)
34
- rspec-expectations (~> 3.9.0)
35
- rspec-mocks (~> 3.9.0)
36
- rspec-core (3.9.0)
37
- rspec-support (~> 3.9.0)
38
- rspec-expectations (3.9.0)
39
- diff-lcs (>= 1.2.0, < 2.0)
40
- rspec-support (~> 3.9.0)
41
- rspec-mocks (3.9.0)
42
- diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.9.0)
44
- rspec-support (3.9.0)
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.1)
50
-
51
- PLATFORMS
52
- ruby
53
-
54
- DEPENDENCIES
55
- bcrypt (~> 3.1)
56
- bundler (~> 2.0)
57
- rake (~> 10.0)
58
- rspec (~> 3.0)
59
- sqlite3 (~> 1.4)
60
- tiny_auth!
61
-
62
- BUNDLED WITH
63
- 2.0.2
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here