zaikio-jwt_auth 0.1.3 → 0.2.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
2
  SHA256:
3
- metadata.gz: 3e888abf5976a9f6837a1723da1f586f317add69e45caa8527c68c086e1f4c40
4
- data.tar.gz: 3ccd16afb7cdc1e808b01dab786cf24cdab31e2eff44bffaa695a0c8231afad5
3
+ metadata.gz: 608b4341e5cb5797a302e65439882e050044fd2676b0fc01f7931783529ee032
4
+ data.tar.gz: 1dcaeb58daa1f352c352b8a3e65bab7e46618c4dba30f64af2141c25bb1f2dee
5
5
  SHA512:
6
- metadata.gz: 36540e2d6588c39f994e4ae5dc62c2bb274d505feb32404f3d0dd7568c57f665024ed164686bc3056c5f236b4471f271917d2f42090d62a3685952010bf27ba9
7
- data.tar.gz: 13447cfaf7386af9cbbfe83f54ccb0ffed13e310c9dd7fab94705ee1b321bf3d82daeb40a4ba9bb273dc313e355aaf822b33f702e5386674c5ce223cd629155f
6
+ metadata.gz: 2289f1f2fc4ddc1a84070f6df75ebbfb143b4b0634ff8a64d3c54cfa5fb2741de8734aaa398989fd9fb63a43dde8ccb4849fa497eee8f6e3a1897acd5fde4dcf
7
+ data.tar.gz: 4b935055a6461f2f2e22dec109634e41b40222773fb363811e76d29327eea22d81210a470e6d9865092473d114c1c21646a99bcbb0942cdc4e39229f7f4e8291
data/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  Gem for JWT-Based authentication and authorization with zaikio.
4
4
 
5
- ## Usage
6
-
7
5
  ## Installation
8
6
 
9
- 1. Add this line to your application's Gemfile:
7
+ ### 1. Add this line to your application's Gemfile:
10
8
 
11
9
  ```ruby
12
10
  gem 'zaikio-jwt_auth'
@@ -22,7 +20,7 @@ Or install it yourself as:
22
20
  $ gem install zaikio-jwt_auth
23
21
  ```
24
22
 
25
- 2. Configure the gem:
23
+ ### 2. Configure the gem:
26
24
 
27
25
  ```rb
28
26
  # config/initializers/zaikio_jwt_auth.rb
@@ -34,7 +32,7 @@ Zaikio::JWTAuth.configure do |config|
34
32
  end
35
33
  ```
36
34
 
37
- 3. Extend your API application controller:
35
+ ### 3. Extend your API application controller:
38
36
 
39
37
  ```rb
40
38
  class API::ApplicationController < ActionController::Base
@@ -49,44 +47,59 @@ class API::ApplicationController < ActionController::Base
49
47
  end
50
48
  ```
51
49
 
52
- 4. Update Revoked Access Tokens by Webhook
50
+ ### 4. Update Revoked Access Tokens by Webhook
51
+
52
+ This gem automatically registers a webhook, if you have properly setup [Zaikio::Webhooks](https://github.com/crispymtn/zaikio-webhooks).
53
+
54
+
55
+ ### 5. Add more restrictions to your resources:
53
56
 
54
57
  ```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
58
+ class API::ResourcesController < API::ApplicationController
59
+ authorize_by_jwt_subject_type 'Organization'
60
+ authorize_by_jwt_scopes 'resources'
61
+ end
62
+ ```
60
63
 
61
- before_action :verify_signature
62
- before_action :update_blacklisted_access_tokens_by_webhook
64
+ ### 6. Optionally, if you are using SSO: Check revoked tokens
63
65
 
64
- def create
65
- case params[:name]
66
- # Manage other events
67
- end
68
- end
66
+ Additionally, the API provides a method called `revoked_jwt?` which expects the `jti` of the JWT.
69
67
 
70
- private
68
+ ```rb
69
+ Zaikio::JWTAuth.revoked_jwt?('jti-of-token') # returns true if token was revoked
70
+ ```
71
71
 
72
- def verify_signature
73
- # Read More: https://docs.zaikio.com/guide/loom/receiving-events.html
74
- unless ActiveSupport::SecurityUtils.secure_compare(
75
- OpenSSL::HMAC.hexdigest("SHA256", "shared-secret", request.body.read),
76
- request.headers["X-Loom-Signature"]
77
- )
78
- render status: :unauthorized, json: { errors: ["invalid_signature"] }
79
- end
80
- end
81
- end
72
+ ### 7. Optionally, use the test helper module to mock JWTs in your minitests
73
+
74
+ ```rb
75
+ # in your test_helper.rb
76
+ include Zaikio::JWTAuth::TestHelper
77
+
78
+ # in your tests you can use:
79
+ mock_jwt(sub: 'Organization/123', scope: ['directory.organization.r'])
82
80
  ```
83
81
 
82
+ ## Advanced
84
83
 
85
- 5. Add more restrictions to your resources:
84
+ ### `only` and `except`
85
+
86
+ Similar to Rails' controller callbacks, `authorize_by_jwt_scopes` can also be passed a list of actions:
86
87
 
87
88
  ```rb
88
89
  class API::ResourcesController < API::ApplicationController
89
90
  authorize_by_jwt_subject_type 'Organization'
90
- authorize_by_jwt_scopes 'resources'
91
+ authorize_by_jwt_scopes 'resources', except: :destroy
92
+ authorize_by_jwt_scopes 'remove_resources', only: [:destroy]
93
+ end
94
+ ```
95
+
96
+
97
+ ### `if` and `unless`
98
+
99
+ Similar to Rails' controller callbacks, `authorize_by_jwt_scopes` can also handle a lambda in the context of the controller to request parameters.
100
+
101
+ ```rb
102
+ class API::ResourcesController < API::ApplicationController
103
+ authorize_by_jwt_scopes 'resources', unless: -> { params[:skip] == '1' }
91
104
  end
92
105
  ```
@@ -0,0 +1,12 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ class RevokeAccessTokenJob < ApplicationJob
4
+ def perform(event)
5
+ DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
6
+ data["blacklisted_token_ids"] << event.payload["access_token_id"]
7
+ data
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,6 +5,8 @@ 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/engine"
9
+ require "zaikio/jwt_auth/test_helper"
8
10
 
9
11
  module Zaikio
10
12
  module JWTAuth
@@ -14,21 +16,51 @@ module Zaikio
14
16
 
15
17
  def self.configure
16
18
  self.configuration ||= Configuration.new
19
+
20
+ if Zaikio.const_defined?("Webhooks")
21
+ Zaikio::Webhooks.on "directory.revoked_access_token", Zaikio::JWTAuth::RevokeAccessTokenJob,
22
+ perform_now: true
23
+ end
24
+
17
25
  yield(configuration)
18
26
  end
19
27
 
28
+ def self.revoked_jwt?(jti)
29
+ blacklisted_token_ids.include?(jti)
30
+ end
31
+
32
+ def self.blacklisted_token_ids
33
+ return [] if mocked_jwt_payload
34
+
35
+ return configuration.blacklisted_token_ids if configuration.blacklisted_token_ids
36
+
37
+ DirectoryCache.fetch("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes)["blacklisted_token_ids"]
38
+ end
39
+
20
40
  def self.included(base)
21
41
  base.send :include, InstanceMethods
22
42
  base.send :extend, ClassMethods
23
43
  end
24
44
 
45
+ def self.mocked_jwt_payload
46
+ @mocked_jwt_payload
47
+ end
48
+
49
+ def self.mocked_jwt_payload=(payload)
50
+ @mocked_jwt_payload = payload
51
+ end
52
+
25
53
  module ClassMethods
26
54
  def authorize_by_jwt_subject_type(type = nil)
27
55
  @authorize_by_jwt_subject_type ||= type
28
56
  end
29
57
 
30
58
  def authorize_by_jwt_scopes(scopes = nil, options = {})
31
- @authorize_by_jwt_scopes ||= options.merge(scopes: scopes)
59
+ @authorize_by_jwt_scopes ||= []
60
+
61
+ @authorize_by_jwt_scopes << options.merge(scopes: scopes) if scopes
62
+
63
+ @authorize_by_jwt_scopes
32
64
  end
33
65
  end
34
66
 
@@ -54,7 +86,7 @@ module Zaikio
54
86
  def update_blacklisted_access_tokens_by_webhook
55
87
  return unless params[:name] == "directory.revoked_access_token"
56
88
 
57
- DirectoryCache.update("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes) do |data|
89
+ DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
58
90
  data["blacklisted_token_ids"] << params[:payload][:access_token_id]
59
91
  data
60
92
  end
@@ -65,19 +97,26 @@ module Zaikio
65
97
  private
66
98
 
67
99
  def jwt_from_auth_header
100
+ return true if Zaikio::JWTAuth.mocked_jwt_payload
101
+
68
102
  auth_header = request.headers["Authorization"]
69
103
  auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
70
104
  end
71
105
 
72
106
  def jwt_payload
107
+ return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
108
+
73
109
  payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
74
110
 
75
111
  payload
76
112
  end
77
113
 
78
114
  def show_error_if_authorize_by_jwt_scopes_fails(token_data)
79
- scope_data = self.class.authorize_by_jwt_scopes
80
- return if !scope_data[:scopes] || token_data.scope?(scope_data[:scopes], action_name, scope_data[:app_name])
115
+ return if token_data.scope_by_configurations?(
116
+ self.class.authorize_by_jwt_scopes,
117
+ action_name,
118
+ self
119
+ )
81
120
 
82
121
  render_error("unpermitted_scope")
83
122
  end
@@ -92,19 +131,11 @@ module Zaikio
92
131
  end
93
132
 
94
133
  def show_error_if_token_is_blacklisted(token_data)
95
- return unless blacklisted_token_ids.include?(token_data.jti)
134
+ return unless Zaikio::JWTAuth.revoked_jwt?(token_data.jti)
96
135
 
97
136
  render_error("invalid_jwt")
98
137
  end
99
138
 
100
- def blacklisted_token_ids
101
- if Zaikio::JWTAuth.configuration.blacklisted_token_ids
102
- return Zaikio::JWTAuth.configuration.blacklisted_token_ids
103
- end
104
-
105
- DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes)["blacklisted_token_ids"]
106
- end
107
-
108
139
  def render_error(error, status: :forbidden)
109
140
  render(status: status, json: { "errors" => [error] })
110
141
  end
@@ -18,6 +18,7 @@ module Zaikio
18
18
 
19
19
  def initialize
20
20
  @environment = :sandbox
21
+ @blacklisted_token_ids = nil
21
22
  end
22
23
 
23
24
  def logger
@@ -0,0 +1,9 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Zaikio::JWTAuth
5
+ engine_name "zaikio_jwt_auth"
6
+ config.generators.api_only = true
7
+ end
8
+ end
9
+ end
@@ -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,6 +33,36 @@ module Zaikio
33
33
  @payload["jti"]
34
34
  end
35
35
 
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
+
36
66
  def scope?(allowed_scopes, action_name, app_name = nil)
37
67
  app_name ||= Zaikio::JWTAuth.configuration.app_name
38
68
  Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
@@ -1,5 +1,5 @@
1
1
  module Zaikio
2
2
  module JWTAuth
3
- VERSION = "0.1.3".freeze
3
+ VERSION = "0.2.0".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.3
4
+ version: 0.2.0
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-02-03 00:00:00.000000000 Z
11
+ date: 2020-03-27 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
@@ -62,12 +62,15 @@ files:
62
62
  - MIT-LICENSE
63
63
  - README.md
64
64
  - Rakefile
65
+ - app/jobs/zaikio/jwt_auth/revoke_access_token_job.rb
65
66
  - lib/tasks/zaikio/jwt_auth_tasks.rake
66
67
  - lib/zaikio/jwt_auth.rb
67
68
  - lib/zaikio/jwt_auth/configuration.rb
68
69
  - lib/zaikio/jwt_auth/directory_cache.rb
70
+ - lib/zaikio/jwt_auth/engine.rb
69
71
  - lib/zaikio/jwt_auth/jwk.rb
70
72
  - lib/zaikio/jwt_auth/railtie.rb
73
+ - lib/zaikio/jwt_auth/test_helper.rb
71
74
  - lib/zaikio/jwt_auth/token_data.rb
72
75
  - lib/zaikio/jwt_auth/version.rb
73
76
  homepage: https://www.zaikio.com/