zaikio-jwt_auth 0.1.2 → 0.1.7

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
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/