zaikio-jwt_auth 0.2.5 → 0.4.2

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: c12ee3b69a3ce0152c39d0af1a5af71bc79ac24651b622e7cfa411c851c8d35f
4
- data.tar.gz: 48e86c7cf283611de01b8754b5125f16545107c576a544c0c91c9d78be8ebac7
3
+ metadata.gz: e7fc3c22cfc4c1f3664674815aa450cea8d3607bfc75c9e0fded0849ac199763
4
+ data.tar.gz: e3452768d900d0f992165e60df3d32743a763da97969a2ddf7b331a60d253f6f
5
5
  SHA512:
6
- metadata.gz: 22cbcb8445e7c8a7bc56aff7516a4dbce5f5763b41a37599fd123957950883af076f5138d40ceef37ee9a0d8d121e36adb924ac1d9d06aaf4f1df4e762cede4d
7
- data.tar.gz: 1d2d5d926d21a79eb734b6ddfd1b4c03f8b7b17b4fc8261bc11dba58722adea46340cb2901c24ac0588553a5d78048145d4ded1e84333246db4e9dfa023b8ca3
6
+ metadata.gz: fde1af79e8a59aabfbe96d3f020f3180881d220a69ce209e4e4b8d82048e91d9d754c39d97d8604dccbde3aa9c02ef5aa526bb7b444d94b98f53c632efed8932
7
+ data.tar.gz: 538fb9c7471206bee6c72725720869d2545ded691ae9901f09c0966d42d1d77eb94dfecf9ebd31935ad982c93512584a99b17a095bd16c7eb237ad084a4c3d7c
data/README.md CHANGED
@@ -49,7 +49,7 @@ end
49
49
 
50
50
  ### 4. Update Revoked Access Tokens by Webhook
51
51
 
52
- This gem automatically registers a webhook, if you have properly setup [Zaikio::Webhooks](https://github.com/crispymtn/zaikio-webhooks).
52
+ This gem automatically registers a webhook, if you have properly setup [Zaikio::Webhooks](https://github.com/zaikio/zaikio-webhooks).
53
53
 
54
54
 
55
55
  ### 5. Add more restrictions to your resources:
@@ -63,6 +63,26 @@ end
63
63
 
64
64
  By convention, `authorize_by_jwt_scopes` automatically maps all CRUD actions in a controller. Requests for `show` and `index` with a read or read_write scope are allowed. All other actions like `create`, `update` and `destroy` are accepted if the scope is a write or read_write scope. Therefore it is strongly recommended to always create standard Rails resources. If a custom action is required, you will need to authorize yourself using the `after_jwt_auth`.
65
65
 
66
+ #### Modifying required scopes
67
+ If you nonetheless want to change the required scopes for CRUD routes, you can use the `type` option which accepts the following values: `:read`, `:write`, `:read_write`
68
+
69
+ ```rb
70
+ class API::ResourcesController < API::ApplicationController
71
+ # Require a write or read_write scope on the index route
72
+ authorize_by_jwt_scopes 'resources', only: :index, type: :write
73
+ end
74
+ ```
75
+
76
+ #### Using custom actions
77
+ You can also specify authorization for custom actions. When doing so the `type` option is required.
78
+
79
+ ```rb
80
+ class API::ResourcesController < API::ApplicationController
81
+ # Require the index use to have a write or read_write scope
82
+ authorize_by_jwt_scopes 'resources', only: :my_custom_route, type: :write
83
+ end
84
+ ```
85
+
66
86
  ### 6. Optionally, if you are using SSO: Check revoked tokens
67
87
 
68
88
  Additionally, the API provides a method called `revoked_jwt?` which expects the `jti` of the JWT.
@@ -118,3 +138,32 @@ class API::ResourcesController < API::ApplicationController
118
138
  authorize_by_jwt_scopes 'resources', unless: -> { params[:skip] == '1' }
119
139
  end
120
140
  ```
141
+
142
+ ### Usage outside a Rails controller
143
+
144
+ If you need to access a JWT outside the normal Rails controllers (e.g. in a Rack
145
+ middleware), there's a static helper method `.extract` which you can use:
146
+
147
+ ```ruby
148
+ class MyRackMiddleware < Rack::Middleware
149
+ def call(env)
150
+ token = Zaikio::JWTAuth.extract(env["HTTP_AUTHORIZATION"])
151
+ puts token.subject_type #=> "Organization"
152
+ ...
153
+ ```
154
+
155
+ This function expects to receive the string in the format `"Bearer $token"`.
156
+
157
+ ## Contributing
158
+
159
+ **Make sure you have the dummy app running locally to validate your changes.**
160
+
161
+ - Make your changes and submit a pull request for them
162
+ - Make sure to update `CHANGELOG.md`
163
+
164
+ To release a new version of the gem:
165
+ - Update the version in `lib/zaikio/jwt_auth/version.rb`
166
+ - Update `CHANGELOG.md` to include the new version and its release date
167
+ - Commit and push your changes
168
+ - Create a [new release on GitHub](https://github.com/zaikio/zaikio-jwt_auth/releases/new)
169
+ - CircleCI will build the Gem package and push it Rubygems for you
@@ -2,8 +2,8 @@ module Zaikio
2
2
  module JWTAuth
3
3
  class RevokeAccessTokenJob < ApplicationJob
4
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"]
5
+ DirectoryCache.update("api/v1/revoked_access_tokens.json", expires_after: 60.minutes) do |data|
6
+ data["revoked_token_ids"] << event.payload["access_token_id"]
7
7
  data
8
8
  end
9
9
  end
@@ -1,5 +1,6 @@
1
1
  require "jwt"
2
2
  require "oj"
3
+ require "active_support/core_ext/integer/time"
3
4
  require "zaikio/jwt_auth/railtie"
4
5
  require "zaikio/jwt_auth/configuration"
5
6
  require "zaikio/jwt_auth/directory_cache"
@@ -26,16 +27,16 @@ module Zaikio
26
27
  end
27
28
 
28
29
  def self.revoked_jwt?(jti)
29
- blacklisted_token_ids.include?(jti)
30
+ revoked_token_ids.include?(jti)
30
31
  end
31
32
 
32
- def self.blacklisted_token_ids
33
+ def self.revoked_token_ids
33
34
  return [] if mocked_jwt_payload
34
35
 
35
- configuration.blacklisted_token_ids || DirectoryCache.fetch(
36
- "api/v1/blacklisted_access_tokens.json",
36
+ configuration.revoked_token_ids || DirectoryCache.fetch(
37
+ "api/v1/revoked_access_tokens.json",
37
38
  expires_after: 60.minutes
38
- )["blacklisted_token_ids"]
39
+ )["revoked_token_ids"]
39
40
  end
40
41
 
41
42
  def self.included(base)
@@ -51,6 +52,20 @@ module Zaikio
51
52
  @mocked_jwt_payload = payload
52
53
  end
53
54
 
55
+ HEADER_FORMAT = /\ABearer (.+)\z/.freeze
56
+
57
+ def self.extract(authorization_header_string)
58
+ return TokenData.new(Zaikio::JWTAuth.mocked_jwt_payload) if Zaikio::JWTAuth.mocked_jwt_payload
59
+
60
+ return if authorization_header_string.blank?
61
+
62
+ return unless (token = authorization_header_string[HEADER_FORMAT, 1])
63
+
64
+ payload, = JWT.decode(token, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
65
+
66
+ TokenData.new(payload)
67
+ end
68
+
54
69
  module ClassMethods
55
70
  def authorize_by_jwt_subject_type(type = nil)
56
71
  @authorize_by_jwt_subject_type ||= type
@@ -67,11 +82,10 @@ module Zaikio
67
82
 
68
83
  module InstanceMethods
69
84
  def authenticate_by_jwt
70
- render_error("no_jwt_passed", status: :unauthorized) && return unless jwt_from_auth_header
71
-
72
- token_data = TokenData.new(jwt_payload)
85
+ token_data = Zaikio::JWTAuth.extract(request.headers["Authorization"])
86
+ return render_error("no_jwt_passed", status: :unauthorized) unless token_data
73
87
 
74
- return if show_error_if_token_is_blacklisted(token_data)
88
+ return if show_error_if_token_is_revoked(token_data)
75
89
 
76
90
  return if show_error_if_authorize_by_jwt_subject_type_fails(token_data)
77
91
 
@@ -84,11 +98,11 @@ module Zaikio
84
98
  render_error("invalid_jwt") && (return)
85
99
  end
86
100
 
87
- def update_blacklisted_access_tokens_by_webhook
101
+ def update_revoked_access_tokens_by_webhook
88
102
  return unless params[:name] == "directory.revoked_access_token"
89
103
 
90
- DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
91
- data["blacklisted_token_ids"] << params[:payload][:access_token_id]
104
+ DirectoryCache.update("api/v1/revoked_access_tokens.json", expires_after: 60.minutes) do |data|
105
+ data["revoked_token_ids"] << params[:payload][:access_token_id]
92
106
  data
93
107
  end
94
108
 
@@ -97,21 +111,6 @@ module Zaikio
97
111
 
98
112
  private
99
113
 
100
- def jwt_from_auth_header
101
- return true if Zaikio::JWTAuth.mocked_jwt_payload
102
-
103
- auth_header = request.headers["Authorization"]
104
- auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
105
- end
106
-
107
- def jwt_payload
108
- return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
109
-
110
- payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
111
-
112
- payload
113
- end
114
-
115
114
  def show_error_if_authorize_by_jwt_scopes_fails(token_data)
116
115
  return if token_data.scope_by_configurations?(
117
116
  self.class.authorize_by_jwt_scopes,
@@ -131,7 +130,7 @@ module Zaikio
131
130
  render_error("unpermitted_subject")
132
131
  end
133
132
 
134
- def show_error_if_token_is_blacklisted(token_data)
133
+ def show_error_if_token_is_revoked(token_data)
135
134
  return unless Zaikio::JWTAuth.revoked_jwt?(token_data.jti)
136
135
 
137
136
  render_error("invalid_jwt")
@@ -4,25 +4,25 @@ module Zaikio
4
4
  module JWTAuth
5
5
  class Configuration
6
6
  HOSTS = {
7
- development: "http://directory.zaikio.test",
8
- test: "http://directory.zaikio.test",
9
- staging: "https://directory.staging.zaikio.com",
10
- sandbox: "https://directory.sandbox.zaikio.com",
11
- production: "https://directory.zaikio.com"
7
+ development: "http://hub.zaikio.test",
8
+ test: "http://hub.zaikio.test",
9
+ staging: "https://hub.staging.zaikio.com",
10
+ sandbox: "https://hub.sandbox.zaikio.com",
11
+ production: "https://hub.zaikio.com"
12
12
  }.freeze
13
13
 
14
- attr_accessor :app_name
15
- attr_accessor :redis, :host
14
+ attr_accessor :app_name, :redis, :host
16
15
  attr_reader :environment
17
- attr_writer :logger, :blacklisted_token_ids, :keys
16
+ attr_writer :logger, :revoked_token_ids, :keys
18
17
 
19
18
  def initialize
20
19
  @environment = :sandbox
21
- @blacklisted_token_ids = nil
20
+ @revoked_token_ids = nil
21
+ @keys = nil
22
22
  end
23
23
 
24
24
  def logger
25
- @logger ||= Logger.new(STDOUT)
25
+ @logger ||= Logger.new($stdout)
26
26
  end
27
27
 
28
28
  def environment=(env)
@@ -31,11 +31,11 @@ module Zaikio
31
31
  end
32
32
 
33
33
  def keys
34
- defined?(@keys) && @keys.is_a?(Proc) ? @keys.call : @keys
34
+ @keys.is_a?(Proc) ? @keys.call : @keys
35
35
  end
36
36
 
37
- def blacklisted_token_ids
38
- @blacklisted_token_ids.is_a?(Proc) ? @blacklisted_token_ids.call : @blacklisted_token_ids
37
+ def revoked_token_ids
38
+ @revoked_token_ids.is_a?(Proc) ? @revoked_token_ids.call : @revoked_token_ids
39
39
  end
40
40
 
41
41
  private
@@ -14,7 +14,7 @@ module Zaikio
14
14
  jti: "unique-access-token-id",
15
15
  nbf: Time.now.to_i,
16
16
  exp: 1.hour.from_now.to_i,
17
- jku: "http://directory.zaikio.test/api/v1/jwt_public_keys.json",
17
+ jku: "http://hub.zaikio.test/api/v1/jwt_public_keys.json",
18
18
  scope: []
19
19
  }.merge(extra_payload).stringify_keys
20
20
  end
@@ -2,7 +2,7 @@ module Zaikio
2
2
  module JWTAuth
3
3
  class TokenData
4
4
  def self.subject_format
5
- %r{^((\w+)/((\w|-)+)\>)?(\w+)/((\w|-)+)$}
5
+ %r{^((\w+)/((\w|-)+)>)?(\w+)/((\w|-)+)$}
6
6
  end
7
7
 
8
8
  def self.actions_by_permission
@@ -13,6 +13,14 @@ module Zaikio
13
13
  }.freeze
14
14
  end
15
15
 
16
+ def self.permissions_by_type
17
+ {
18
+ read: %w[r rw],
19
+ write: %w[rw w],
20
+ read_write: %w[r rw w]
21
+ }
22
+ end
23
+
16
24
  def initialize(payload)
17
25
  @payload = payload
18
26
  end
@@ -33,9 +41,13 @@ module Zaikio
33
41
  @payload["jti"]
34
42
  end
35
43
 
44
+ def expires_at
45
+ Time.zone.at(@payload["exp"]).to_datetime
46
+ end
47
+
36
48
  # 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)
49
+ # scope, app_name (optional), except/only (array, optional), type (read, write, readwrite)
50
+ def scope_by_configurations?(scope_configurations, action_name, context) # rubocop:disable Metrics/AbcSize
39
51
  configuration = scope_configurations.find do |scope_configuration|
40
52
  action_matches = action_matches_config?(scope_configuration, action_name)
41
53
 
@@ -50,7 +62,7 @@ module Zaikio
50
62
 
51
63
  return true unless configuration
52
64
 
53
- scope?(configuration[:scopes], action_name, configuration[:app_name])
65
+ scope?(configuration[:scopes], action_name, app_name: configuration[:app_name], type: configuration[:type])
54
66
  end
55
67
 
56
68
  def action_matches_config?(scope_configuration, action_name)
@@ -63,14 +75,14 @@ module Zaikio
63
75
  end
64
76
  end
65
77
 
66
- def scope?(allowed_scopes, action_name, app_name = nil)
78
+ def scope?(allowed_scopes, action_name, app_name: nil, type: nil)
67
79
  app_name ||= Zaikio::JWTAuth.configuration.app_name
68
80
  Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
69
81
  scope.any? do |s|
70
82
  parts = s.split(".")
71
83
  parts[0] == app_name &&
72
84
  parts[1] == allowed_scope &&
73
- action_in_permission?(action_name, parts[2])
85
+ action_permitted?(action_name, parts[2], type: type)
74
86
  end
75
87
  end
76
88
  end
@@ -97,8 +109,14 @@ module Zaikio
97
109
 
98
110
  private
99
111
 
100
- def action_in_permission?(action_name, permission)
101
- self.class.actions_by_permission[permission].include?(action_name)
112
+ def action_permitted?(action_name, permission, type: nil)
113
+ if type
114
+ return false unless self.class.permissions_by_type.key?(type)
115
+
116
+ self.class.permissions_by_type[type].include?(permission)
117
+ else
118
+ self.class.actions_by_permission[permission].include?(action_name)
119
+ end
102
120
  end
103
121
  end
104
122
  end
@@ -1,5 +1,5 @@
1
1
  module Zaikio
2
2
  module JWTAuth
3
- VERSION = "0.2.5".freeze
3
+ VERSION = "0.4.2".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zaikio-jwt_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - crispymtn
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-06-04 00:00:00.000000000 Z
13
+ date: 2021-02-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: oj
@@ -78,10 +78,12 @@ files:
78
78
  - lib/zaikio/jwt_auth/test_helper.rb
79
79
  - lib/zaikio/jwt_auth/token_data.rb
80
80
  - lib/zaikio/jwt_auth/version.rb
81
- homepage: https://www.zaikio.com/
81
+ homepage: https://github.com/zaikio/zaikio-jwt_auth
82
82
  licenses:
83
83
  - MIT
84
- metadata: {}
84
+ metadata:
85
+ changelog_uri: https://github.com/zaikio/zaikio-jwt_auth/blob/main/CHANGELOG.md
86
+ source_code_uri: https://github.com/zaikio/zaikio-jwt_auth
85
87
  post_install_message:
86
88
  rdoc_options: []
87
89
  require_paths:
@@ -90,14 +92,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
92
  requirements:
91
93
  - - ">="
92
94
  - !ruby/object:Gem::Version
93
- version: '0'
95
+ version: 2.6.5
94
96
  required_rubygems_version: !ruby/object:Gem::Requirement
95
97
  requirements:
96
98
  - - ">="
97
99
  - !ruby/object:Gem::Version
98
100
  version: '0'
99
101
  requirements: []
100
- rubygems_version: 3.0.3
102
+ rubygems_version: 3.1.4
101
103
  signing_key:
102
104
  specification_version: 4
103
105
  summary: JWT-Based authentication and authorization with zaikio