zaikio-jwt_auth 0.1.4 → 0.2.1

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: 590caa5aad36a46fd406e074f022ea4b89b22c9e701673cdc2d755de3cbbb15f
4
- data.tar.gz: 16d71074b163e50dd491028e7c59cd3bcab6a6b4817fa6a53ff1e6420d5f5fa3
3
+ metadata.gz: 025f45746c3a1a8f50fe821e580f5b41e3ae0126bf450a785488941200e2b0bf
4
+ data.tar.gz: 0ded26e92d822a537c8238f01f13ae3a51f3fc6e2e48863b88455ec25d26dd7b
5
5
  SHA512:
6
- metadata.gz: d194f56893bcaf2682397e4879c3ffad102146bd7ac78f08c3676ccb854fbf258a938a9f59c3b2338ce299b3c4b4b1ac54b991c37da52f71211b0f615494ad84
7
- data.tar.gz: 9264c519c109d025f38bc4f95c9c4543e5b3d084d8bbe6241a42e42a3e70235b036f0fe7b63c21a44b2f5e89b2490268262aa237d6aa0e7cd28a5cbbe6f9567d
6
+ metadata.gz: 819316b9aa09aa764ce734b1f177da3aeb108c9b5b7e0cfb3426aac674dbc13115df3767d2cf933321241c88ff0721caecebef4eb7be630d04882e2a7c91eac2
7
+ data.tar.gz: 762dd6bb4b7e0abd44cc4333d6c5799ed2802d893875cab1808f690f46f10a6a3844fbe8a1c26004895846375be4832ad4afd218cf84ebf9eb79abe993a9714b
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,46 +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
66
+ Additionally, the API provides a method called `revoked_jwt?` which expects the `jti` of the JWT.
68
67
 
69
- render json: { received: true }
70
- end
68
+ ```rb
69
+ Zaikio::JWTAuth.revoked_jwt?('jti-of-token') # returns true if token was revoked
70
+ ```
71
71
 
72
- private
72
+ ### 7. Optionally, use the test helper module to mock JWTs in your minitests
73
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
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'])
84
80
  ```
85
81
 
82
+ ## Advanced
83
+
84
+ ### `only` and `except`
86
85
 
87
- 5. Add more restrictions to your resources:
86
+ Similar to Rails' controller callbacks, `authorize_by_jwt_scopes` can also be passed a list of actions:
88
87
 
89
88
  ```rb
90
89
  class API::ResourcesController < API::ApplicationController
91
90
  authorize_by_jwt_subject_type 'Organization'
92
- 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' }
93
104
  end
94
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
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'JWT'
3
+ 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
 
@@ -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,20 +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_access_tokens.json",
106
- expires_after: 60.minutes)["blacklisted_token_ids"]
107
- end
108
-
109
139
  def render_error(error, status: :forbidden)
110
140
  render(status: status, json: { "errors" => [error] })
111
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.4".freeze
3
+ VERSION = "0.2.1".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.4
4
+ version: 0.2.1
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-04 00:00:00.000000000 Z
11
+ date: 2020-04-02 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,16 @@ files:
62
62
  - MIT-LICENSE
63
63
  - README.md
64
64
  - Rakefile
65
+ - app/jobs/zaikio/jwt_auth/revoke_access_token_job.rb
66
+ - config/initializers/inflections.rb
65
67
  - lib/tasks/zaikio/jwt_auth_tasks.rake
66
68
  - lib/zaikio/jwt_auth.rb
67
69
  - lib/zaikio/jwt_auth/configuration.rb
68
70
  - lib/zaikio/jwt_auth/directory_cache.rb
71
+ - lib/zaikio/jwt_auth/engine.rb
69
72
  - lib/zaikio/jwt_auth/jwk.rb
70
73
  - lib/zaikio/jwt_auth/railtie.rb
74
+ - lib/zaikio/jwt_auth/test_helper.rb
71
75
  - lib/zaikio/jwt_auth/token_data.rb
72
76
  - lib/zaikio/jwt_auth/version.rb
73
77
  homepage: https://www.zaikio.com/