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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +35 -0
- data/.gem_release.yml +5 -0
- data/.github/CODEOWNERS +4 -0
- data/.github/stale.yml +17 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.rubocop.yml +16 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +33 -0
- data/README.md +71 -2
- data/Rakefile +4 -28
- data/_config.yml +1 -0
- data/app/decorators/controllers/solidus_jwt/spree/api/base_controller_decorator.rb +41 -0
- data/app/decorators/models/solidus_jwt/spree/user_decorator.rb +58 -0
- data/app/models/solidus_jwt/base_record.rb +13 -0
- data/app/models/solidus_jwt/token.rb +63 -0
- data/bin/console +17 -0
- data/bin/r +9 -0
- data/bin/rails +8 -0
- data/bin/rails-engine +15 -0
- data/bin/rails-sandbox +17 -0
- data/bin/rake +7 -0
- data/bin/sandbox +84 -0
- data/bin/sandbox_rails +9 -0
- data/bin/setup +8 -0
- data/config/locales/en.yml +2 -1
- data/config/routes.rb +3 -0
- data/db/migrate/20190222220038_create_solidus_jwt_tokens.rb +14 -0
- data/db/migrate/20191212083655_add_foreign_key_to_users_table.rb +7 -0
- data/lib/controllers/api/spree/api/oauths_controller.rb +47 -0
- data/lib/generators/solidus_jwt/install/install_generator.rb +3 -11
- data/lib/solidus_jwt.rb +8 -0
- data/lib/solidus_jwt/concerns/decodeable.rb +7 -1
- data/lib/solidus_jwt/concerns/encodeable.rb +14 -2
- data/lib/solidus_jwt/config.rb +2 -0
- data/lib/solidus_jwt/devise_strategies/base.rb +25 -0
- data/lib/solidus_jwt/devise_strategies/password.rb +46 -0
- data/lib/solidus_jwt/devise_strategies/refresh_token.rb +48 -0
- data/lib/solidus_jwt/distributor/devise.rb +3 -1
- data/lib/solidus_jwt/engine.rb +8 -12
- data/lib/solidus_jwt/factories.rb +13 -0
- data/lib/solidus_jwt/preferences.rb +8 -0
- data/lib/solidus_jwt/version.rb +3 -9
- data/solidus_jwt.gemspec +38 -0
- data/spec/lib/solidus_jwt/concerns/decodeable_spec.rb +0 -0
- data/spec/lib/solidus_jwt/concerns/encodeable_spec.rb +0 -0
- data/spec/lib/solidus_jwt/config_spec.rb +7 -0
- data/spec/lib/solidus_jwt/devise_strategies/password_spec.rb +78 -0
- data/spec/lib/solidus_jwt/devise_strategies/refresh_token_spec.rb +74 -0
- data/spec/lib/solidus_jwt/preferences_spec.rb +39 -0
- data/spec/lib/solidus_jwt_spec.rb +8 -0
- data/spec/models/solidus_jwt/token_spec.rb +43 -0
- data/spec/requests/spree/api/json_web_tokens_spec.rb +77 -0
- data/spec/requests/spree/api/oauths_spec.rb +122 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/shared_examples/decodeable_examples.rb +23 -0
- data/spec/support/shared_examples/encodeable_examples.rb +29 -0
- metadata +86 -222
- data/app/assets/javascripts/spree/backend/solidus_jwt.js +0 -2
- data/app/assets/javascripts/spree/frontend/solidus_jwt.js +0 -2
- data/app/assets/stylesheets/spree/backend/solidus_jwt.css +0 -4
- data/app/assets/stylesheets/spree/frontend/solidus_jwt.css +0 -4
- data/app/controllers/spree/api/base_controller/json_web_tokens.rb +0 -22
- data/app/controllers/spree/api/base_controller_decorator.rb +0 -17
- data/app/models/spree/user_decorator.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5069a0d35cb2f777cae543265907fdd284e8e42ccb0e86c8bc4775e17beb6a88
|
4
|
+
data.tar.gz: 1019df1fc49a245eb54d5b64e1f52956a405c4f4f1003a043d65d81b871d672a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/.gem_release.yml
ADDED
data/.github/CODEOWNERS
ADDED
data/.github/stale.yml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
solidus_jwt
|
data/.ruby-version
ADDED
@@ -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 =
|
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
|
-
###
|
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
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'solidus_dev_support/rake_tasks'
|
4
|
+
SolidusDevSupport::RakeTasks.install
|
4
5
|
|
5
|
-
|
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'
|
data/_config.yml
ADDED
@@ -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
|