zaikio-jwt_auth 0.1.2 → 0.1.7

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
2
  SHA256:
3
- metadata.gz: 38efe9a261965fba37dd9bedfb98646dbc50943715ae3be8c643ddb9c067650b
4
- data.tar.gz: 46f84150b23a8437ea5d080be5eefea054df8c7fde522593a7e445532b77578a
3
+ metadata.gz: aa5cedf782b4972795398012fb5d50f5156995a34951a077b49ab8c72d47af6c
4
+ data.tar.gz: 2e76b158eaddb1c7993412883fe0926af121ac1e959c8f0ff6e9fdc4a71fd17c
5
5
  SHA512:
6
- metadata.gz: ca226ab262494c8f3619905834dc1309693df9abec13b408999eb6a6f3c9ddc12fda702394fe822b21c7960382628c2f9546db269c663f925d56de8f418badad
7
- data.tar.gz: 8c55b455367af7f7e8a25ac9e78357ce208ce345e8f54f8e7f201c0bda68284e7ea04a16eae5aad1798d4b1856bd421d803192efb4c1adf478f7d1789633db72
6
+ metadata.gz: 0c837c489820a0bfe172813a22e0a974bb5686724826db11a76c88cf08085c2ca250124310dedc95146e640b6755ad32e18490a35cf617270c736ab63fbe46b1
7
+ data.tar.gz: da668d14415c81d38a08b8d1bed52390b7040476301ac049a43c63a8e96eb973b229ebd5ad689b215b244b99dd18edbc0e3ff78f24eb84d1245babb977b09cdf
data/README.md CHANGED
@@ -6,7 +6,7 @@ Gem for JWT-Based authentication and authorization with zaikio.
6
6
 
7
7
  ## Installation
8
8
 
9
- Add this line to your application's Gemfile:
9
+ 1. Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
12
  gem 'zaikio-jwt_auth'
@@ -22,7 +22,7 @@ Or install it yourself as:
22
22
  $ gem install zaikio-jwt_auth
23
23
  ```
24
24
 
25
- Configure the gem:
25
+ 2. Configure the gem:
26
26
 
27
27
  ```rb
28
28
  # config/initializers/zaikio_jwt_auth.rb
@@ -34,7 +34,7 @@ Zaikio::JWTAuth.configure do |config|
34
34
  end
35
35
  ```
36
36
 
37
- Extend your API application controller:
37
+ 3. Extend your API application controller:
38
38
 
39
39
  ```rb
40
40
  class API::ApplicationController < ActionController::Base
@@ -49,7 +49,42 @@ class API::ApplicationController < ActionController::Base
49
49
  end
50
50
  ```
51
51
 
52
- Add more restrictions to your resources:
52
+ 4. Update Revoked Access Tokens by Webhook
53
+
54
+ ```rb
55
+ # ENV['ZAIKIO_SHARED_SECRET'] needs to be defined first, you can find it on your
56
+ # app details page in zaikio. Fore more help read:
57
+ # https://docs.zaikio.com/guide/loom/receiving-events.html
58
+ class WebhooksController < ActionController::Base
59
+ include Zaikio::JWTAuth
60
+
61
+ before_action :verify_signature
62
+ before_action :update_blacklisted_access_tokens_by_webhook
63
+
64
+ def create
65
+ case params[:name]
66
+ # Manage other events
67
+ end
68
+
69
+ render json: { received: true }
70
+ end
71
+
72
+ private
73
+
74
+ def verify_signature
75
+ # Read More: https://docs.zaikio.com/guide/loom/receiving-events.html
76
+ unless ActiveSupport::SecurityUtils.secure_compare(
77
+ OpenSSL::HMAC.hexdigest("SHA256", "shared-secret", request.body.read),
78
+ request.headers["X-Loom-Signature"]
79
+ )
80
+ render json: { received: true }
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+
87
+ 5. Add more restrictions to your resources:
53
88
 
54
89
  ```rb
55
90
  class API::ResourcesController < API::ApplicationController
@@ -57,3 +92,21 @@ class API::ResourcesController < API::ApplicationController
57
92
  authorize_by_jwt_scopes 'resources'
58
93
  end
59
94
  ```
95
+
96
+ 6. Optionally, if you are using SSO: Check revoked tokens
97
+
98
+ Additionally, the API provides a method called `revoked_jwt?` which expects the `jti` of the JWT.
99
+
100
+ ```rb
101
+ Zaikio::JWTAuth.revoked_jwt?('jti-of-token') # returns true if token was revoked
102
+ ```
103
+
104
+ 7. Optionally, use the test helper module to mock JWTs in your minitests
105
+
106
+ ```rb
107
+ # in your test_helper.rb
108
+ include Zaikio::JWTAuth::TestHelper
109
+
110
+ # in your tests you can use:
111
+ mock_jwt(sub: 'Organization/123', scope: ['directory.organization.r'])
112
+ ```
@@ -5,6 +5,7 @@ require "zaikio/jwt_auth/configuration"
5
5
  require "zaikio/jwt_auth/directory_cache"
6
6
  require "zaikio/jwt_auth/jwk"
7
7
  require "zaikio/jwt_auth/token_data"
8
+ require "zaikio/jwt_auth/test_helper"
8
9
 
9
10
  module Zaikio
10
11
  module JWTAuth
@@ -17,18 +18,42 @@ module Zaikio
17
18
  yield(configuration)
18
19
  end
19
20
 
21
+ def self.revoked_jwt?(jti)
22
+ blacklisted_token_ids.include?(jti)
23
+ end
24
+
25
+ def self.blacklisted_token_ids
26
+ return [] if mocked_jwt_payload
27
+
28
+ return configuration.blacklisted_token_ids if configuration.blacklisted_token_ids
29
+
30
+ DirectoryCache.fetch("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes)["blacklisted_token_ids"]
31
+ end
32
+
20
33
  def self.included(base)
21
34
  base.send :include, InstanceMethods
22
35
  base.send :extend, ClassMethods
23
36
  end
24
37
 
38
+ def self.mocked_jwt_payload
39
+ @mocked_jwt_payload
40
+ end
41
+
42
+ def self.mocked_jwt_payload=(payload)
43
+ @mocked_jwt_payload = payload
44
+ end
45
+
25
46
  module ClassMethods
26
47
  def authorize_by_jwt_subject_type(type = nil)
27
48
  @authorize_by_jwt_subject_type ||= type
28
49
  end
29
50
 
30
- def authorize_by_jwt_scopes(scopes = nil)
31
- @authorize_by_jwt_scopes ||= scopes
51
+ def authorize_by_jwt_scopes(scopes = nil, options = {})
52
+ @authorize_by_jwt_scopes ||= []
53
+
54
+ @authorize_by_jwt_scopes << options.merge(scopes: scopes) if scopes
55
+
56
+ @authorize_by_jwt_scopes
32
57
  end
33
58
  end
34
59
 
@@ -51,23 +76,40 @@ module Zaikio
51
76
  render_error("invalid_jwt") && (return)
52
77
  end
53
78
 
79
+ def update_blacklisted_access_tokens_by_webhook
80
+ return unless params[:name] == "directory.revoked_access_token"
81
+
82
+ DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
83
+ data["blacklisted_token_ids"] << params[:payload][:access_token_id]
84
+ data
85
+ end
86
+
87
+ render json: { received: true }
88
+ end
89
+
54
90
  private
55
91
 
56
92
  def jwt_from_auth_header
93
+ return true if Zaikio::JWTAuth.mocked_jwt_payload
94
+
57
95
  auth_header = request.headers["Authorization"]
58
96
  auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
59
97
  end
60
98
 
61
99
  def jwt_payload
100
+ return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
101
+
62
102
  payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
63
103
 
64
104
  payload
65
105
  end
66
106
 
67
107
  def show_error_if_authorize_by_jwt_scopes_fails(token_data)
68
- if !self.class.authorize_by_jwt_scopes || token_data.scope?(self.class.authorize_by_jwt_scopes, action_name)
69
- return
70
- end
108
+ return if token_data.scope_by_configurations?(
109
+ self.class.authorize_by_jwt_scopes,
110
+ action_name,
111
+ self
112
+ )
71
113
 
72
114
  render_error("unpermitted_scope")
73
115
  end
@@ -82,19 +124,11 @@ module Zaikio
82
124
  end
83
125
 
84
126
  def show_error_if_token_is_blacklisted(token_data)
85
- return unless blacklisted_token_ids.include?(token_data.jti)
127
+ return unless Zaikio::JWTAuth.revoked_jwt?(token_data.jti)
86
128
 
87
129
  render_error("invalid_jwt")
88
130
  end
89
131
 
90
- def blacklisted_token_ids
91
- if Zaikio::JWTAuth.configuration.blacklisted_token_ids
92
- return Zaikio::JWTAuth.configuration.blacklisted_token_ids
93
- end
94
-
95
- DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 5.minutes)["blacklisted_token_ids"]
96
- end
97
-
98
132
  def render_error(error, status: :forbidden)
99
133
  render(status: status, json: { "errors" => [error] })
100
134
  end
@@ -18,6 +18,19 @@ module Zaikio
18
18
  json["data"]
19
19
  end
20
20
 
21
+ def update(directory_path, options = {})
22
+ data = fetch(directory_path, options)
23
+ data = yield(data)
24
+ Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
25
+ fetched_at: Time.now.to_i,
26
+ data: data
27
+ }.to_json)
28
+ end
29
+
30
+ def reset(directory_path)
31
+ Zaikio::JWTAuth.configuration.redis.del("zaikio::jwt_auth::#{directory_path}")
32
+ end
33
+
21
34
  private
22
35
 
23
36
  def cache_expired?(json, expires_after)
@@ -0,0 +1,23 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ module TestHelper
4
+ def after_setup
5
+ Zaikio::JWTAuth.mocked_jwt_payload = nil
6
+ super
7
+ end
8
+
9
+ def mock_jwt(extra_payload)
10
+ Zaikio::JWTAuth.mocked_jwt_payload = {
11
+ iss: "ZAI",
12
+ sub: nil,
13
+ aud: %w[test_app],
14
+ jti: "unique-access-token-id",
15
+ nbf: Time.now.to_i,
16
+ exp: 1.hour.from_now.to_i,
17
+ jku: "http://directory.zaikio.test/api/v1/jwt_public_keys.json",
18
+ scope: []
19
+ }.merge(extra_payload).stringify_keys
20
+ end
21
+ end
22
+ end
23
+ end
@@ -33,11 +33,42 @@ module Zaikio
33
33
  @payload["jti"]
34
34
  end
35
35
 
36
- def scope?(allowed_scopes, action_name)
36
+ # scope_options is an array of objects with:
37
+ # scope, app_name (optional), except/only (array, optional)
38
+ def scope_by_configurations?(scope_configurations, action_name, context)
39
+ configuration = scope_configurations.find do |scope_configuration|
40
+ action_matches = action_matches_config?(scope_configuration, action_name)
41
+
42
+ if action_matches && scope_configuration[:if] && !context.instance_exec(&scope_configuration[:if])
43
+ false
44
+ elsif action_matches && scope_configuration[:unless] && context.instance_exec(&scope_configuration[:unless])
45
+ false
46
+ else
47
+ action_matches
48
+ end
49
+ end
50
+
51
+ return true unless configuration
52
+
53
+ scope?(configuration[:scopes], action_name, configuration[:app_name])
54
+ end
55
+
56
+ def action_matches_config?(scope_configuration, action_name)
57
+ if scope_configuration[:only]
58
+ Array(scope_configuration[:only]).any? { |a| a.to_s == action_name }
59
+ elsif scope_configuration[:except]
60
+ Array(scope_configuration[:except]).none? { |a| a.to_s == action_name }
61
+ else
62
+ true
63
+ end
64
+ end
65
+
66
+ def scope?(allowed_scopes, action_name, app_name = nil)
67
+ app_name ||= Zaikio::JWTAuth.configuration.app_name
37
68
  Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
38
69
  scope.any? do |s|
39
70
  parts = s.split(".")
40
- parts[0] == Zaikio::JWTAuth.configuration.app_name &&
71
+ parts[0] == app_name &&
41
72
  parts[1] == allowed_scope &&
42
73
  action_in_permission?(action_name, parts[2])
43
74
  end
@@ -1,5 +1,5 @@
1
1
  module Zaikio
2
2
  module JWTAuth
3
- VERSION = "0.1.2".freeze
3
+ VERSION = "0.1.7".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zaikio-jwt_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Crispy Mountain GmbH
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-28 00:00:00.000000000 Z
11
+ date: 2020-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 6.0.1
33
+ version: 6.0.2.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 6.0.1
40
+ version: 6.0.2.2
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: jwt
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -68,6 +68,7 @@ files:
68
68
  - lib/zaikio/jwt_auth/directory_cache.rb
69
69
  - lib/zaikio/jwt_auth/jwk.rb
70
70
  - lib/zaikio/jwt_auth/railtie.rb
71
+ - lib/zaikio/jwt_auth/test_helper.rb
71
72
  - lib/zaikio/jwt_auth/token_data.rb
72
73
  - lib/zaikio/jwt_auth/version.rb
73
74
  homepage: https://www.zaikio.com/