zaikio-jwt_auth 0.1.0 → 0.1.5

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: 6de8bcc2c7ef22d052c626bc463789dec9dba4c6d4e977ee543973736994440b
4
- data.tar.gz: c3c1b661b388fdef157831bccd203616913456f594762c4d0513967ebde8c468
3
+ metadata.gz: '0559a98a2221978a8a5c35e6968d667d9c17287b1421500ae0062bad169c0ece'
4
+ data.tar.gz: be9a95f5684dd8329f1b5446400ad8704f46422a364de01675538f3001c0bf59
5
5
  SHA512:
6
- metadata.gz: 694d2a013c3a8e41dc8b03a11b0817fadd8e636cd80809a4d66c04285b2ecfe1e2dcaaf919f7735556a001d6d6feb93c4541a1f61bce16689e117fbabe6bf55f
7
- data.tar.gz: 44c62f3c08102efb72e0ca8951562d02481bf5882742cd75ff4a27f722d80713f939c17cf05271cd870ef8b5fae3c00ad534b756aa981c32399e634a366b674d
6
+ metadata.gz: d4f1a51bed19e09d9ca09eb873377639f6f197f289742d74e17a6d6fba3e130608547530ca65daa6ab40c3faad4a00a8b695582fee084d78b8e96e897165301f
7
+ data.tar.gz: bf0eab6e6761239b33ae00d565ffc0b884fd70ddea3d589a91a0a1c7b2bf124e955048e8a5eebfbc7e42d1b37ed41fd96b0d6139b5f19ce5b60358bf69525612
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,26 +18,44 @@ 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 ||= options.merge(scopes: scopes)
32
53
  end
33
54
  end
34
55
 
35
56
  module InstanceMethods
36
57
  def authenticate_by_jwt
37
- unless jwt_from_auth_header
38
- render(status: :unauthorized, plain: "Please authenticate via Zaikio JWT") && return
39
- end
58
+ render_error("no_jwt_passed", status: :unauthorized) && return unless jwt_from_auth_header
40
59
 
41
60
  token_data = TokenData.new(jwt_payload)
42
61
 
@@ -48,30 +67,44 @@ module Zaikio
48
67
 
49
68
  send(:after_jwt_auth, token_data) if respond_to?(:after_jwt_auth)
50
69
  rescue JWT::ExpiredSignature
51
- render(status: :forbidden, plain: "JWT expired") && (return)
70
+ render_error("jwt_expired") && (return)
52
71
  rescue JWT::DecodeError
53
- render(status: :forbidden, plain: "Invalid JWT") && (return)
72
+ render_error("invalid_jwt") && (return)
73
+ end
74
+
75
+ def update_blacklisted_access_tokens_by_webhook
76
+ return unless params[:name] == "directory.revoked_access_token"
77
+
78
+ DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
79
+ data["blacklisted_token_ids"] << params[:payload][:access_token_id]
80
+ data
81
+ end
82
+
83
+ render json: { received: true }
54
84
  end
55
85
 
56
86
  private
57
87
 
58
88
  def jwt_from_auth_header
89
+ return true if Zaikio::JWTAuth.mocked_jwt_payload
90
+
59
91
  auth_header = request.headers["Authorization"]
60
92
  auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
61
93
  end
62
94
 
63
95
  def jwt_payload
96
+ return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
97
+
64
98
  payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
65
99
 
66
100
  payload
67
101
  end
68
102
 
69
103
  def show_error_if_authorize_by_jwt_scopes_fails(token_data)
70
- if !self.class.authorize_by_jwt_scopes || token_data.scope?(self.class.authorize_by_jwt_scopes, action_name)
71
- return
72
- end
104
+ scope_data = self.class.authorize_by_jwt_scopes
105
+ return if !scope_data[:scopes] || token_data.scope?(scope_data[:scopes], action_name, scope_data[:app_name])
73
106
 
74
- render(status: :forbidden, plain: "Invalid scope")
107
+ render_error("unpermitted_scope")
75
108
  end
76
109
 
77
110
  def show_error_if_authorize_by_jwt_subject_type_fails(token_data)
@@ -80,21 +113,17 @@ module Zaikio
80
113
  return
81
114
  end
82
115
 
83
- render(status: :forbidden, plain: "Unallowed subject type")
116
+ render_error("unpermitted_subject")
84
117
  end
85
118
 
86
119
  def show_error_if_token_is_blacklisted(token_data)
87
- return unless blacklisted_token_ids.include?(token_data.jti)
120
+ return unless Zaikio::JWTAuth.revoked_jwt?(token_data.jti)
88
121
 
89
- render(status: :forbidden, plain: "Invalid token")
122
+ render_error("invalid_jwt")
90
123
  end
91
124
 
92
- def blacklisted_token_ids
93
- if Zaikio::JWTAuth.configuration.blacklisted_token_ids
94
- return Zaikio::JWTAuth.configuration.blacklisted_token_ids
95
- end
96
-
97
- DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 5.minutes)["blacklisted_token_ids"]
125
+ def render_error(error, status: :forbidden)
126
+ render(status: status, json: { "errors" => [error] })
98
127
  end
99
128
  end
100
129
  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
@@ -17,6 +17,14 @@ module Zaikio
17
17
  @payload = payload
18
18
  end
19
19
 
20
+ def audience
21
+ audiences.first
22
+ end
23
+
24
+ def audiences
25
+ @payload["aud"] || []
26
+ end
27
+
20
28
  def scope
21
29
  @payload["scope"]
22
30
  end
@@ -25,11 +33,12 @@ module Zaikio
25
33
  @payload["jti"]
26
34
  end
27
35
 
28
- def scope?(allowed_scopes, action_name)
36
+ def scope?(allowed_scopes, action_name, app_name = nil)
37
+ app_name ||= Zaikio::JWTAuth.configuration.app_name
29
38
  Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
30
39
  scope.any? do |s|
31
40
  parts = s.split(".")
32
- parts[0] == Zaikio::JWTAuth.configuration.app_name &&
41
+ parts[0] == app_name &&
33
42
  parts[1] == allowed_scope &&
34
43
  action_in_permission?(action_name, parts[2])
35
44
  end
@@ -1,5 +1,5 @@
1
1
  module Zaikio
2
2
  module JWTAuth
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.1.5".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.0
4
+ version: 0.1.5
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-20 00:00:00.000000000 Z
11
+ date: 2020-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -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/