zaikio-jwt_auth 0.3.0 → 0.4.3

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: 5e75e96ec3854a6fcaad1f5d4dcc01f92ba444741c34af7de567427cfeec6159
4
- data.tar.gz: 9250c75142635ac6eb4a8f31f7c5b16c541493cf660bf99da268f5233ddb4485
3
+ metadata.gz: 4ac13b8d1a727e9aefeef7e1da082a3eec4bf48ac8f149ef4cc89ac44ee35d49
4
+ data.tar.gz: 323355b6f5180bf9662b715c8d2cf8ed94311fa1095e5da2627c8a71cd0f442a
5
5
  SHA512:
6
- metadata.gz: b6c823c6798566123fcf7fc20b1c40e98bc1acf00314b91e1d6b8d8b645716b347bad5179f58e002484c95945fdcafcc4a30a3f7ffe9843ca46e2ca35583d91d
7
- data.tar.gz: 9c9ebfb94fb8b9c2f62f42a9e2bafad58918830f20463dab24dd209cf7a9a33a270f0a52b8f2ac8d2339bc71097736ada9766baadd840d671231ddbefa2a7d60
6
+ metadata.gz: 386ec7f2a8f1bc0a0d2f29495cc4b5754cd2eadce857555eb68a72f9546f76095ba2f0f0c3fdbaa5982a2503ee400480e81418dea5a06e85265380384843e350
7
+ data.tar.gz: 044a91585bf67a5ba411716ea361a1c85eeab91ef06cbe17b1cd1f03c83a261c0a4e8ef5ebe1e87e3c9e550678fdbcae45859448031c5e5b2983affbb829fd0f
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,43 @@ 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"`. If the JWT is
156
+ invalid, expired, or has some other fundamental issues, the JWT library may throw
157
+ [additional errors](https://github.com/jwt/ruby-jwt/blob/v2.2.2/lib/jwt/error.rb), and you
158
+ should be prepared to handle these, for example:
159
+
160
+ ```ruby
161
+ def call(env)
162
+ token = Zaikio::JWTAuth.extract("definitely.not.jwt")
163
+ rescue JWT::DecodeError, JWT::ExpiredSignature
164
+ [401, {}, ["Unauthorized"]]
165
+ end
166
+ ```
167
+
168
+ ## Contributing
169
+
170
+ **Make sure you have the dummy app running locally to validate your changes.**
171
+
172
+ - Make your changes and submit a pull request for them
173
+ - Make sure to update `CHANGELOG.md`
174
+
175
+ To release a new version of the gem:
176
+ - Update the version in `lib/zaikio/jwt_auth/version.rb`
177
+ - Update `CHANGELOG.md` to include the new version and its release date
178
+ - Commit and push your changes
179
+ - Create a [new release on GitHub](https://github.com/zaikio/zaikio-jwt_auth/releases/new)
180
+ - CircleCI will build the Gem package and push it Rubygems for you
@@ -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"
@@ -17,7 +18,7 @@ module Zaikio
17
18
  def self.configure
18
19
  self.configuration ||= Configuration.new
19
20
 
20
- if Zaikio.const_defined?("Webhooks")
21
+ if Zaikio.const_defined?("Webhooks", false)
21
22
  Zaikio::Webhooks.on "directory.revoked_access_token", Zaikio::JWTAuth::RevokeAccessTokenJob,
22
23
  perform_now: true
23
24
  end
@@ -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,9 +82,8 @@ 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
88
  return if show_error_if_token_is_revoked(token_data)
75
89
 
@@ -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,
@@ -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
16
  attr_writer :logger, :revoked_token_ids, :keys
18
17
 
19
18
  def initialize
20
19
  @environment = :sandbox
21
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,7 +31,7 @@ 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
37
  def revoked_token_ids
@@ -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.3.0".freeze
3
+ VERSION = "0.4.3".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.3.0
4
+ version: 0.4.3
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-09 00:00:00.000000000 Z
13
+ date: 2021-03-17 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