zaikio-jwt_auth 0.1.3 → 0.2.0

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