solidus_jwt 0.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +35 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/CODEOWNERS +4 -0
  5. data/.github/stale.yml +17 -0
  6. data/.gitignore +19 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +16 -0
  9. data/.ruby-gemset +1 -0
  10. data/.ruby-version +1 -0
  11. data/Gemfile +33 -0
  12. data/README.md +71 -2
  13. data/Rakefile +4 -28
  14. data/_config.yml +1 -0
  15. data/app/decorators/controllers/solidus_jwt/spree/api/base_controller_decorator.rb +41 -0
  16. data/app/decorators/models/solidus_jwt/spree/user_decorator.rb +58 -0
  17. data/app/models/solidus_jwt/base_record.rb +13 -0
  18. data/app/models/solidus_jwt/token.rb +63 -0
  19. data/bin/console +17 -0
  20. data/bin/r +9 -0
  21. data/bin/rails +8 -0
  22. data/bin/rails-engine +15 -0
  23. data/bin/rails-sandbox +17 -0
  24. data/bin/rake +7 -0
  25. data/bin/sandbox +84 -0
  26. data/bin/sandbox_rails +9 -0
  27. data/bin/setup +8 -0
  28. data/config/locales/en.yml +2 -1
  29. data/config/routes.rb +3 -0
  30. data/db/migrate/20190222220038_create_solidus_jwt_tokens.rb +14 -0
  31. data/db/migrate/20191212083655_add_foreign_key_to_users_table.rb +7 -0
  32. data/lib/controllers/api/spree/api/oauths_controller.rb +47 -0
  33. data/lib/generators/solidus_jwt/install/install_generator.rb +3 -11
  34. data/lib/solidus_jwt.rb +8 -0
  35. data/lib/solidus_jwt/concerns/decodeable.rb +7 -1
  36. data/lib/solidus_jwt/concerns/encodeable.rb +14 -2
  37. data/lib/solidus_jwt/config.rb +2 -0
  38. data/lib/solidus_jwt/devise_strategies/base.rb +25 -0
  39. data/lib/solidus_jwt/devise_strategies/password.rb +46 -0
  40. data/lib/solidus_jwt/devise_strategies/refresh_token.rb +48 -0
  41. data/lib/solidus_jwt/distributor/devise.rb +3 -1
  42. data/lib/solidus_jwt/engine.rb +8 -12
  43. data/lib/solidus_jwt/factories.rb +13 -0
  44. data/lib/solidus_jwt/preferences.rb +8 -0
  45. data/lib/solidus_jwt/version.rb +3 -9
  46. data/solidus_jwt.gemspec +38 -0
  47. data/spec/lib/solidus_jwt/concerns/decodeable_spec.rb +0 -0
  48. data/spec/lib/solidus_jwt/concerns/encodeable_spec.rb +0 -0
  49. data/spec/lib/solidus_jwt/config_spec.rb +7 -0
  50. data/spec/lib/solidus_jwt/devise_strategies/password_spec.rb +78 -0
  51. data/spec/lib/solidus_jwt/devise_strategies/refresh_token_spec.rb +74 -0
  52. data/spec/lib/solidus_jwt/preferences_spec.rb +39 -0
  53. data/spec/lib/solidus_jwt_spec.rb +8 -0
  54. data/spec/models/solidus_jwt/token_spec.rb +43 -0
  55. data/spec/requests/spree/api/json_web_tokens_spec.rb +77 -0
  56. data/spec/requests/spree/api/oauths_spec.rb +122 -0
  57. data/spec/spec_helper.rb +26 -0
  58. data/spec/support/shared_examples/decodeable_examples.rb +23 -0
  59. data/spec/support/shared_examples/encodeable_examples.rb +29 -0
  60. metadata +86 -222
  61. data/app/assets/javascripts/spree/backend/solidus_jwt.js +0 -2
  62. data/app/assets/javascripts/spree/frontend/solidus_jwt.js +0 -2
  63. data/app/assets/stylesheets/spree/backend/solidus_jwt.css +0 -4
  64. data/app/assets/stylesheets/spree/frontend/solidus_jwt.css +0 -4
  65. data/app/controllers/spree/api/base_controller/json_web_tokens.rb +0 -22
  66. data/app/controllers/spree/api/base_controller_decorator.rb +0 -17
  67. data/app/models/spree/user_decorator.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cf6dd72fda604868c1589819000a6d1607a8078fa642ce2a17cb071695b26f1
4
- data.tar.gz: ca122c197a880c21398f6bcf28a2e8dd7709f90613e0fe41c4f24ea166802e09
3
+ metadata.gz: 5069a0d35cb2f777cae543265907fdd284e8e42ccb0e86c8bc4775e17beb6a88
4
+ data.tar.gz: 1019df1fc49a245eb54d5b64e1f52956a405c4f4f1003a043d65d81b871d672a
5
5
  SHA512:
6
- metadata.gz: c1a2929d69915b28aec1df8c2a4fa450191ae0c465c57e5b7992938a88c9fb6363ae71282930b4dcea644add482d54c3406eb471a93a0c74a520ad8e11e9e98e
7
- data.tar.gz: b7938c410ed7e14bc8098aa7314490beaff2bfc29b86249c72c9a2411f48b3a2b71441b6c17a159fff864caaf1923e348ba5b01c5acfcc570be3c60c1e75a571
6
+ metadata.gz: b042053167cadec496c99b501b12ada4a3090ea7163afa15718a768b9b01ab822ff8a6f139ffd8c93377525d26b0c83917acb19944ad485b161dd5162a14f1bf
7
+ data.tar.gz: c8266151ccbd5d50042c289e1ccd2993854dc305e364576489d08a38ab0a0b42166a93a1ac454aac438f306cb46785055cca600003c50bbdf1f8eadd288e72fa
@@ -0,0 +1,35 @@
1
+ version: 2.1
2
+
3
+ orbs:
4
+ # Always take the latest version of the orb, this allows us to
5
+ # run specs against Solidus supported versions only without the need
6
+ # to change this configuration every time a Solidus version is released
7
+ # or goes EOL.
8
+ solidusio_extensions: solidusio/extensions@volatile
9
+
10
+ jobs:
11
+ run-specs-with-postgres:
12
+ executor: solidusio_extensions/postgres
13
+ steps:
14
+ - solidusio_extensions/run-tests
15
+ run-specs-with-mysql:
16
+ executor: solidusio_extensions/mysql
17
+ steps:
18
+ - solidusio_extensions/run-tests
19
+
20
+ workflows:
21
+ "Run specs on supported Solidus versions":
22
+ jobs:
23
+ - run-specs-with-postgres
24
+ - run-specs-with-mysql
25
+ "Weekly run specs against master":
26
+ triggers:
27
+ - schedule:
28
+ cron: "0 0 * * 4" # every Thursday
29
+ filters:
30
+ branches:
31
+ only:
32
+ - master
33
+ jobs:
34
+ - run-specs-with-postgres
35
+ - run-specs-with-mysql
@@ -0,0 +1,5 @@
1
+ bump:
2
+ recurse: false
3
+ file: 'lib/solidus_jwt/version.rb'
4
+ message: Bump SolidusDemo1 to %{version}
5
+ tag: true
@@ -0,0 +1,4 @@
1
+ ##
2
+ # Default Code Owner
3
+ ##
4
+ * @skukx
@@ -0,0 +1,17 @@
1
+ # Number of days of inactivity before an issue becomes stale
2
+ daysUntilStale: 60
3
+ # Number of days of inactivity before a stale issue is closed
4
+ daysUntilClose: 7
5
+ # Issues with these labels will never be considered stale
6
+ exemptLabels:
7
+ - pinned
8
+ - security
9
+ # Label to use when marking an issue as stale
10
+ staleLabel: wontfix
11
+ # Comment to post when marking an issue as stale. Set to `false` to disable
12
+ markComment: >
13
+ This issue has been automatically marked as stale because it has not had
14
+ recent activity. It will be closed if no further activity occurs. Thank you
15
+ for your contributions.
16
+ # Comment to post when closing a stale issue. Set to `false` to disable
17
+ closeComment: false
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ \#*
3
+ *~
4
+ .#*
5
+ .DS_Store
6
+ .idea
7
+ .project
8
+ .sass-cache
9
+ coverage
10
+ Gemfile.lock
11
+ tmp
12
+ nbproject
13
+ pkg
14
+ *.swp
15
+ spec/dummy
16
+ spec/examples.txt
17
+
18
+ sandbox/
19
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,16 @@
1
+ require:
2
+ - solidus_dev_support/rubocop
3
+
4
+ AllCops:
5
+ Exclude:
6
+ - sandbox/**/*
7
+ - spec/dummy/**/*
8
+
9
+ Metrics/LineLength:
10
+ Enabled: false
11
+
12
+ RSpec/MultipleExpectations:
13
+ Enabled: false
14
+
15
+ RSpec/NestedGroups:
16
+ Enabled: false
@@ -0,0 +1 @@
1
+ solidus_jwt
@@ -0,0 +1 @@
1
+ 2.6.5
data/Gemfile ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
+
6
+ branch = ENV.fetch('SOLIDUS_BRANCH', 'master')
7
+ gem 'solidus', github: 'solidusio/solidus', branch: branch
8
+
9
+ # Needed to help Bundler figure out how to resolve dependencies,
10
+ # otherwise it takes forever to resolve them.
11
+ # See https://github.com/bundler/bundler/issues/6677
12
+ gem 'rails', '>0.a'
13
+
14
+ # Provides basic authentication functionality for testing parts of your engine
15
+ gem 'solidus_auth_devise'
16
+
17
+ case ENV['DB']
18
+ when 'mysql'
19
+ gem 'mysql2'
20
+ when 'postgresql'
21
+ gem 'pg'
22
+ else
23
+ gem 'sqlite3'
24
+ end
25
+
26
+ gemspec
27
+
28
+ # Use a local Gemfile to include development dependencies that might not be
29
+ # relevant for the project or for other contributors, e.g. pry-byebug.
30
+ #
31
+ # We use `send` instead of calling `eval_gemfile` to work around an issue with
32
+ # how Dependabot parses projects: https://github.com/dependabot/dependabot-core/issues/1658.
33
+ send(:eval_gemfile, 'Gemfile-local') if File.exist? 'Gemfile-local'
data/README.md CHANGED
@@ -36,8 +36,9 @@ SolidusJwt::Config.configure do |config|
36
36
  config.jwt_secret = 'secret'
37
37
  config.allow_spree_api_key = true
38
38
  config.jwt_algorithm = 'HS256'
39
- config.jwt_expiration = 3600
39
+ config.jwt_expiration = 3_600
40
40
  config.jwt_options = { only: %i[email first_name id last_name] }
41
+ config.refresh_expiration = 2_592_000
41
42
  end
42
43
  ```
43
44
 
@@ -56,6 +57,9 @@ Defaults to `3600` (1 hour). The amount of time in seconds that the token should
56
57
  #### `jwt_options`
57
58
  Defaults to `{ only: %i[email first_name id last_name] }`. These options are passed into `Spree::User#as_json` when serializing the token's payload. Keep in mind that the more information included, the larger the token will be. It may be in your best interest to keep it short and simple.
58
59
 
60
+ #### `refresh_expiration`:
61
+ Defaults to `2592000` (30 days). The amount of time in seconds that the token should last for.
62
+
59
63
  Usage
60
64
  -------------
61
65
  ### Generating and decoding a token:
@@ -83,7 +87,72 @@ SolidusJwt.decode(token)
83
87
  # ]
84
88
  ```
85
89
 
86
- ### Distributing a Token Using 'solidus_auth_devise':
90
+ ### Autenticate through the API
91
+
92
+ If authenticating through the API, you must have
93
+ [solidus_auth_devise](https://github.com/solidusio/solidus_auth_devise) setup
94
+ because `solidus_jwt` piggybacks off of the [Devise](https://github.com/plataformatec/devise)
95
+ gem. This enables authentication through a single point. If you implement
96
+ [Devise Lockable](https://www.rubydoc.info/github/plataformatec/devise/master/Devise/Models/Lockable),
97
+ then locking is respected both on the front-end as well as on the API.
98
+
99
+ ```ruby
100
+ POST /oauth/token
101
+ {
102
+ "username": "user@example.com"
103
+ "password": "secret"
104
+ "grant_type": "password"
105
+ }
106
+
107
+ # { "access_token": "abc.123.efg", "refresh_token": "123456" }
108
+ ```
109
+
110
+ You can now use the `access_token` to authentication with the
111
+ [Solidus API](https://github.com/solidusio/solidus/tree/master/api) in place
112
+ of the `spree_api_key`.
113
+
114
+ #### Matching token to a user
115
+
116
+ By default, the token matches a user using the `Spree::User.for_jwt` method. This methods
117
+ Finds a user by id using the subject claim of the token. If you want to customize how the
118
+ subject claim is interpreted you can override this method
119
+
120
+ ```ruby
121
+ def self.for_jwt(sub)
122
+ # find_by(id: sub)
123
+ find_by(my_external_id: sub)
124
+ end
125
+ ```
126
+
127
+ ### Obtain a refresh token
128
+
129
+ To refresh your access token, instead of re-authenticating you can send
130
+ a refresh token.
131
+
132
+ ```ruby
133
+ POST /oauth/token
134
+ {
135
+ "refresh_token": "123456"
136
+ "grant_type": "refresh_token"
137
+ }
138
+
139
+ # { "access_token": "hij.456.klm", "refresh_token": "789abc" }
140
+ ```
141
+
142
+ ### Invalidate refresh tokens for a user
143
+
144
+ It is good practice set the lifetime of an access token to be short. In case an
145
+ access token is compromised, the attacker will only have access for a short time.
146
+
147
+ To force a user to have to reauthencate rather than using a refresh token,
148
+ you can do the following:
149
+
150
+ ```ruby
151
+ # Invalidate all refresh tokens for a user
152
+ SolidusJwt::Token.invalidate(user)
153
+ ```
154
+
155
+ ### Distributing a Token Using 'solidus_auth_devise' on front-end:
87
156
 
88
157
  To have the `solidus_auth_devise` gem distribute a token back to the client
89
158
  you can do the following:
data/Rakefile CHANGED
@@ -1,30 +1,6 @@
1
- require 'bundler'
1
+ # frozen_string_literal: true
2
2
 
3
- Bundler::GemHelper.install_tasks
3
+ require 'solidus_dev_support/rake_tasks'
4
+ SolidusDevSupport::RakeTasks.install
4
5
 
5
- begin
6
- require 'spree/testing_support/extension_rake'
7
- require 'rubocop/rake_task'
8
- require 'rspec/core/rake_task'
9
-
10
- RSpec::Core::RakeTask.new(:spec)
11
-
12
- RuboCop::RakeTask.new
13
-
14
- task default: %i(first_run rubocop spec)
15
- rescue LoadError
16
- # no rspec available
17
- end
18
-
19
- task :first_run do
20
- if Dir['spec/dummy'].empty?
21
- Rake::Task[:test_app].invoke
22
- Dir.chdir('../../')
23
- end
24
- end
25
-
26
- desc 'Generates a dummy app for testing'
27
- task :test_app do
28
- ENV['LIB_NAME'] = 'solidus_jwt'
29
- Rake::Task['extension:test_app'].invoke
30
- end
6
+ task default: 'extension:specs'
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-cayman
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusJwt
4
+ module Spree
5
+ module Api
6
+ module BaseControllerDecorator
7
+ def self.prepended(base)
8
+ base.rescue_from JWT::DecodeError do
9
+ render "spree/api/errors/invalid_api_key", status: :unauthorized
10
+ end
11
+ end
12
+
13
+ ##
14
+ # Overrides Solidus
15
+ # @see https://github.com/solidusio/solidus/blob/master/api/app/controllers/spree/api/base_controller.rb
16
+ #
17
+ def load_user
18
+ return super if json_web_token.blank?
19
+
20
+ # rubocop:disable Naming/MemoizedInstanceVariableName
21
+ @current_api_user ||= ::Spree.user_class.for_jwt(json_web_token['sub'] || json_web_token['id'])
22
+ # rubocop:enable Naming/MemoizedInstanceVariableName
23
+ end
24
+
25
+ def json_web_token
26
+ @json_web_token ||= SolidusJwt.decode(api_key).first
27
+ rescue JWT::DecodeError
28
+ # Allow spree to try and authenticate if we still allow it. Otherwise
29
+ # raise an error
30
+ return if SolidusJwt::Config.allow_spree_api_key
31
+
32
+ raise
33
+ end
34
+
35
+ if SolidusSupport.api_available?
36
+ ::Spree::Api::BaseController.prepend self
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusJwt
4
+ module Spree
5
+ module UserDecorator
6
+ def self.prepended(base)
7
+ base.extend ClassMethods
8
+ base.has_many :auth_tokens, class_name: 'SolidusJwt::Token'
9
+ end
10
+
11
+ module ClassMethods
12
+ ##
13
+ # Find user based on subject claim in
14
+ # our json web token
15
+ # @see https://tools.ietf.org/html/rfc7519#section-4.1.2
16
+ #
17
+ # @example get user token
18
+ # payload = SolidusJwt.decode(token).first
19
+ # user = Spree::User.for_jwt(payload['sub'])
20
+ #
21
+ # @param sub [string] The subject claim of jwt
22
+ # @return [Spree.user_class, NilClass] If a match is found, returns the user,
23
+ # otherwise, returns nil
24
+ #
25
+ def for_jwt(sub)
26
+ find_by(id: sub)
27
+ end
28
+ end
29
+
30
+ ##
31
+ # Generate a json web token
32
+ # @see https://github.com/jwt/ruby-jwt
33
+ # @return [String]
34
+ #
35
+ def generate_jwt(expires_in: nil)
36
+ SolidusJwt.encode(payload: as_jwt_payload, expires_in: expires_in)
37
+ end
38
+ alias generate_jwt_token generate_jwt
39
+
40
+ ##
41
+ # Serializes user attributes to hash and applies
42
+ # the sub jwt claim.
43
+ #
44
+ # @return [Hash] The payload for json web token
45
+ #
46
+ def as_jwt_payload
47
+ options = SolidusJwt::Config.jwt_options
48
+ claims = { sub: id }
49
+
50
+ as_json(options)
51
+ .merge(claims)
52
+ .as_json
53
+ end
54
+
55
+ ::Spree.user_class.prepend self
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusJwt
4
+ base_class = defined?(::ApplicationRecord) ? ::ApplicationRecord : ActiveRecord::Base
5
+
6
+ class BaseRecord < base_class
7
+ self.abstract_class = true
8
+
9
+ def self.table_name_prefix
10
+ 'solidus_jwt_'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusJwt
4
+ class Token < BaseRecord
5
+ attr_readonly :token
6
+ enum auth_type: { refresh_token: 0, access_token: 1 }
7
+
8
+ # rubocop:disable Rails/ReflectionClassName
9
+ belongs_to :user, class_name: ::Spree::UserClassHandle.new
10
+ # rubocop:enable Rails/ReflectionClassName
11
+
12
+ scope :non_expired, -> {
13
+ where(
14
+ 'solidus_jwt_tokens.created_at >= ?',
15
+ SolidusJwt::Config.refresh_expiration.seconds.ago
16
+ )
17
+ }
18
+
19
+ enum auth_type: { refresh: 0, access: 1 }
20
+
21
+ validates :token, presence: true
22
+
23
+ before_validation(on: :create) do
24
+ self.token ||= SecureRandom.uuid
25
+ end
26
+
27
+ ##
28
+ # Set all non expired refresh tokens to inactive
29
+ #
30
+ def self.invalidate(user)
31
+ # rubocop:disable Rails/SkipsModelValidations
32
+ non_expired.
33
+ where(user_id: user.to_param).
34
+ update_all(active: false)
35
+ # rubocop:enable Rails/SkipsModelValidations
36
+ end
37
+
38
+ ##
39
+ # Whether to honor a token or not.
40
+ # @return [Boolean]
41
+ #
42
+ def self.honor?(token)
43
+ non_expired.where(active: true).find_by(token: token).present?
44
+ end
45
+
46
+ ##
47
+ # Whether the token should be honored.
48
+ # @return [Boolean] Will be true if the token is active and not expired.
49
+ # Otherwise false.
50
+ def honor?
51
+ active? && !expired?
52
+ end
53
+
54
+ ##
55
+ # Whether the token is expired
56
+ # @return [Boolean] If the token is older than the configured refresh
57
+ # expiration amount then will be true. Otherwise false.
58
+ #
59
+ def expired?
60
+ created_at < SolidusJwt::Config.refresh_expiration.seconds.ago
61
+ end
62
+ end
63
+ end