zaikio-jwt_auth 0.3.0 → 0.4.3

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