tiny_auth 0.2.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: d2c08fa6193667861a991bb4442e3669e32d559a8fd6de39001e9bcef25857f0
4
- data.tar.gz: 7edd1dc4569763af9f68e19066bcc1c0f8d273976f123d01b0a2439414fdd5ae
2
+ SHA1:
3
+ metadata.gz: ab849294a31f7be0e5da2033ab81e9bd91850e67
4
+ data.tar.gz: cd1c89e452847d5b48cf69235c9e0c46f0ecd1ea
5
5
  SHA512:
6
- metadata.gz: ce1f2b71ff644335d65683a044f75280f89bc752b7cd0754d5103ce93b921d051a8fdcc5b7298cec43d32fa516eb0ae03ab6f3a181cab2724a59715d59fb4beb
7
- data.tar.gz: ecfe0672b60c176ea9e8687cb5690af60207814da193a08ec4a7800bad2da3743667061768ce7a245a4557862a6cb3a1ea0d01d4619f88a57c1e271c1a7dd7e6
6
+ metadata.gz: 399030a4ad8844703aee70fa18185fc36f65e566941a1c8fd01a39c36ebe4773604a4ee270e00a5656153f010e3c6dc0bff02af8c410a01b58a530d7049edf32
7
+ data.tar.gz: 63dbff0e1f10d646a14939ba3c6d199860027aae7660657bc9450c90d29c2e4c6b5cc1697637d87bc7ec067d48e075f335a69690bf2208aa3770f88823bf5dd7
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,4 +2,6 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in tiny_auth.gemspec
4
4
  gemspec
5
- gem 'simplecov', require: false, group: :test
5
+
6
+ gem "simplecov"
7
+ gem "coveralls"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # TinyAuth [![Build Status](https://travis-ci.org/rzane/tiny_auth.svg?branch=master)](https://travis-ci.org/rzane/tiny_auth)
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
 
@@ -22,10 +22,11 @@ First, create a table to store your users:
22
22
  create_table :users do |t|
23
23
  t.string :email, null: false
24
24
  t.string :password_digest, null: false
25
- t.string :reset_token
25
+ t.string :reset_token_digest
26
26
  t.datetime :reset_token_expires_at
27
+
27
28
  t.index :email, unique: true
28
- t.index :reset_token, unique: true
29
+ t.index :reset_token_digest, unique: true
29
30
  end
30
31
  ```
31
32
 
@@ -33,32 +34,41 @@ Your model should look like this:
33
34
 
34
35
  ```ruby
35
36
  class User < ApplicationRecord
37
+ include TinyAuth::Model
36
38
  has_secure_password
37
39
  end
38
40
  ```
39
41
 
40
- Now, you're ready to use `TinyAuth`:
42
+ Now, you're ready to authenticate!
41
43
 
42
44
  ```ruby
43
- auth = TinyAuth.new(User)
44
-
45
- user = auth.find_by_email('user@example.com')
46
- user = auth.find_by_credentials('user@example.com', 'password')
45
+ user = User.find_by_email("user@example.com")
46
+ user = User.find_by_credentials("user@example.com", "password")
47
47
 
48
- token = auth.generate_token(user)
49
- user = auth.find_by_token(token)
48
+ token = user.generate_token
49
+ user = User.find_by_token(token)
50
50
 
51
- reset_token = auth.generate_reset_token(user)
52
- user = auth.exchange_reset_token(user, password: "changed")
51
+ reset_token = user.generate_reset_token
52
+ user = User.exchange_reset_token(reset_token)
53
53
  ```
54
54
 
55
- ## Development
56
-
57
- After checking out the repo, run `bundle install` to install dependencies.
58
-
59
- Then, run `bundle exec rspec` to run the tests.
55
+ Oh, and you can add authentication to your controllers:
60
56
 
61
- 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).
57
+ ```ruby
58
+ class ApplicationController < ActionController::Base
59
+ extend TinyAuth::Controller
60
+
61
+ authenticates model: User
62
+
63
+ def index
64
+ if user_signed_in?
65
+ render json: {id: current_user.id}
66
+ else
67
+ head :unauthorized
68
+ end
69
+ end
70
+ end
71
+ ```
62
72
 
63
73
  ## Contributing
64
74
 
@@ -1,76 +1,42 @@
1
+ require "openssl"
2
+ require "tiny_auth/model"
3
+ require "tiny_auth/controller"
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
- def initialize(model, scope: model, secret: secret_key_base)
8
- @model = model
9
- @scope = scope
10
- @secret = secret
11
-
12
- raise ArgumentError, "missing argument: model" if model.nil?
13
- raise ArgumentError, "missing keyword: secret" if secret.nil?
14
- end
15
-
16
- def find_by_email(email)
17
- scope.find_by(model.arel_table[:email].lower.eq(email.downcase))
18
- end
19
-
20
- def find_by_credentials(email, password)
21
- resource = find_by_email(email)
22
- resource if resource&.authenticate(password)
23
- end
24
-
25
- def generate_token(resource, purpose: :access, expires_in: 24.hours)
26
- resource.to_sgid(expires_in: expires_in, for: purpose).to_s
27
- end
28
-
29
- def find_by_token(token, purpose: :access)
30
- GlobalID::Locator.locate_signed(token, for: purpose)
31
- rescue ActiveRecord::RecordNotFound
32
- end
33
-
34
- def generate_reset_token(resource, **opts)
35
- generate_single_use_token(resource, purpose: :reset, **opts)
36
- end
37
-
38
- def generate_single_use_token(resource, purpose:, expires_in: 2.hours)
39
- token = SecureRandom.base58(24)
40
-
41
- resource.update!(
42
- "#{purpose}_token" => hmac(token),
43
- "#{purpose}_token_expires_at" => expires_in.from_now
44
- )
45
-
46
- token
47
- end
48
-
49
- def exchange_reset_token(token, **opts, &block)
50
- exchange_single_use_token(token, purpose: :reset, **opts, &block)
51
- end
52
-
53
- def exchange_single_use_token(token, purpose:, update: {})
54
- not_expired = model.arel_table[:"#{purpose}_token_expires_at"].gt(Time.now)
55
- resource = scope.where(not_expired).find_by(:"#{purpose}_token" => hmac(token))
56
-
57
- return if resource.nil?
58
- yield resource if block_given?
59
-
60
- resource.assign_attributes(update)
61
- resource.update!("#{purpose}_token" => nil, "#{purpose}_token_expires_at" => nil)
62
- resource
63
- end
64
-
65
- private
66
-
67
- attr_reader :model, :scope, :secret
68
-
69
- def hmac(value)
70
- OpenSSL::HMAC.hexdigest("SHA256", secret, value)
71
- end
72
-
73
- def secret_key_base
74
- Rails.application.secret_key_base if defined?(Rails)
6
+ module TinyAuth
7
+ class << self
8
+ # A secret that is used for hashing tokens.
9
+ #
10
+ # If `Rails` is defined, it will attempt to use
11
+ # `Rails.application.secret_key_base`.
12
+ #
13
+ # @raise [RuntimeError]
14
+ # @return [String]
15
+ def secret
16
+ @secret || secret_key_base || missing_secret!
17
+ end
18
+
19
+ # Configure the secret that is used for hashing tokens.
20
+ # @param secret [String]
21
+ def secret=(secret)
22
+ @secret = secret
23
+ end
24
+
25
+ # Create a hash from a value using the secret
26
+ # @param value [String]
27
+ # @return [String]
28
+ def hexdigest(value)
29
+ OpenSSL::HMAC.hexdigest("SHA256", secret, value)
30
+ end
31
+
32
+ private
33
+
34
+ def secret_key_base
35
+ Rails.application.secret_key_base if defined? Rails
36
+ end
37
+
38
+ def missing_secret!
39
+ raise "You need to configure TinyAuth.secret"
40
+ end
75
41
  end
76
42
  end
@@ -0,0 +1,58 @@
1
+ require "active_support/core_ext/object/blank"
2
+
3
+ module TinyAuth
4
+ module Controller
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
+ # @param options [Hash] Additional arguments for `before_action`
19
+ #
20
+ # @example
21
+ # class ApplicationController < ActionController::Base
22
+ # extend TinyAuth::Controller
23
+ #
24
+ # authenticates model: User, only: :index
25
+ #
26
+ # def index
27
+ # if user_signed_in?
28
+ # render json: current_user
29
+ # else
30
+ # head :unauthorized
31
+ # end
32
+ # end
33
+ # end
34
+ def authenticates(model:, name: model.model_name.singular, **options)
35
+ authenticate = :"authenticate_#{name}"
36
+ current = :"current_#{name}"
37
+ current_ivar = :"@current_#{name}"
38
+ signed_in = :"#{name}_signed_in?"
39
+
40
+ attr_reader current
41
+
42
+ define_method(signed_in) do
43
+ !send(current).nil?
44
+ end
45
+
46
+ define_method(authenticate) do
47
+ token = TinyAuth::Controller.token(request)
48
+
49
+ if token
50
+ resource = model.find_by_token(token)
51
+ instance_variable_set(current_ivar, resource)
52
+ end
53
+ end
54
+
55
+ before_action(authenticate, **options)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,75 @@
1
+ require "active_record"
2
+ require "globalid"
3
+ require "active_support/core_ext/numeric/time"
4
+ require "active_support/core_ext/securerandom"
5
+
6
+ module TinyAuth
7
+ module Model
8
+ def self.included(base)
9
+ base.extend ClassMethods
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
+ # @return [ActiveRecord::Base,nil]
33
+ def find_by_token(token)
34
+ resource = GlobalID::Locator.locate_signed(token, for: :access)
35
+ resource if resource.kind_of?(self)
36
+ rescue ActiveRecord::RecordNotFound
37
+ end
38
+
39
+ # Finds a resource by their reset token and nillifies `reset_password_digest`
40
+ # and `reset_token_expires_at` fields
41
+ # @param token [String]
42
+ # @return [ActiveRecord::Base,nil]
43
+ def exchange_reset_token(token)
44
+ digest = TinyAuth.hexdigest(token)
45
+ not_expired = arel_table[:reset_token_expires_at].gt(Time.now)
46
+ resource = where(not_expired).find_by(reset_token_digest: digest)
47
+ resource&.reset_token_digest = nil
48
+ resource&.reset_token_expires_at = nil
49
+ resource
50
+ end
51
+ end
52
+
53
+ # Generates a stateless token for a resource
54
+ # @param expires_in [ActiveSupport::Duration] defaults to 24 hours
55
+ def generate_token(expires_in: 24.hours)
56
+ to_signed_global_id(expires_in: expires_in, for: :access).to_s
57
+ end
58
+
59
+ # Generates a reset token for a resource. A hashed version of the token
60
+ # is stored in the database
61
+ # @param expires_in [ActiveSupport::Duration] defaults to 2 hours
62
+ def generate_reset_token(expires_in: 2.hours)
63
+ token = SecureRandom.base58(24)
64
+ digest = TinyAuth.hexdigest(token)
65
+ expiry = expires_in.from_now
66
+
67
+ update_columns(
68
+ reset_token_digest: digest,
69
+ reset_token_expires_at: expiry
70
+ )
71
+
72
+ token
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,3 @@
1
- class TinyAuth
2
- VERSION = "0.2.0"
1
+ module TinyAuth
2
+ VERSION = "1.0.0"
3
3
  end
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.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Zane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-16 00:00:00.000000000 Z
11
+ date: 2019-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -134,11 +134,12 @@ files:
134
134
  - ".travis.yml"
135
135
  - CODE_OF_CONDUCT.md
136
136
  - Gemfile
137
- - Gemfile.lock
138
137
  - LICENSE.txt
139
138
  - README.md
140
139
  - Rakefile
141
140
  - lib/tiny_auth.rb
141
+ - lib/tiny_auth/controller.rb
142
+ - lib/tiny_auth/model.rb
142
143
  - lib/tiny_auth/version.rb
143
144
  - tiny_auth.gemspec
144
145
  homepage: https://github.com/rzane/tiny_auth
@@ -163,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
164
  version: '0'
164
165
  requirements: []
165
166
  rubyforge_project:
166
- rubygems_version: 2.7.6
167
+ rubygems_version: 2.6.14
167
168
  signing_key:
168
169
  specification_version: 4
169
170
  summary: Bare-minimum authentication for APIs
@@ -1,71 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- tiny_auth (0.1.2)
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
- docile (1.3.2)
27
- globalid (0.4.2)
28
- activesupport (>= 4.2.0)
29
- i18n (1.7.0)
30
- concurrent-ruby (~> 1.0)
31
- json (2.2.0)
32
- minitest (5.13.0)
33
- rake (10.5.0)
34
- rspec (3.9.0)
35
- rspec-core (~> 3.9.0)
36
- rspec-expectations (~> 3.9.0)
37
- rspec-mocks (~> 3.9.0)
38
- rspec-core (3.9.0)
39
- rspec-support (~> 3.9.0)
40
- rspec-expectations (3.9.0)
41
- diff-lcs (>= 1.2.0, < 2.0)
42
- rspec-support (~> 3.9.0)
43
- rspec-mocks (3.9.0)
44
- diff-lcs (>= 1.2.0, < 2.0)
45
- rspec-support (~> 3.9.0)
46
- rspec-support (3.9.0)
47
- simplecov (0.17.0)
48
- docile (~> 1.1)
49
- json (>= 1.8, < 3)
50
- simplecov-html (~> 0.10.0)
51
- simplecov-html (0.10.2)
52
- sqlite3 (1.4.1)
53
- thread_safe (0.3.6)
54
- tzinfo (1.2.5)
55
- thread_safe (~> 0.1)
56
- zeitwerk (2.2.1)
57
-
58
- PLATFORMS
59
- ruby
60
-
61
- DEPENDENCIES
62
- bcrypt (~> 3.1)
63
- bundler (~> 2.0)
64
- rake (~> 10.0)
65
- rspec (~> 3.0)
66
- simplecov
67
- sqlite3 (~> 1.4)
68
- tiny_auth!
69
-
70
- BUNDLED WITH
71
- 2.0.2