zaikio-jwt_auth 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +61 -1
- data/lib/zaikio/jwt_auth.rb +17 -19
- data/lib/zaikio/jwt_auth/configuration.rb +2 -1
- data/lib/zaikio/jwt_auth/directory_cache.rb +10 -3
- data/lib/zaikio/jwt_auth/token_data.rb +21 -7
- data/lib/zaikio/jwt_auth/version.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 452c158575a59584224535f6ea124abfa659139e33cb66534cfefe6ac6819916
|
4
|
+
data.tar.gz: c004d38935525597ca3c3c229ddaf5863b467eb326534dc42aeefe153ab1a245
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bdb4a90f6fdf5d7a004a1b0342ab40eed28a94fbec8f952601af10f10531213c95dcbe815e077b5bc5f46edf935e2f76317ab27a2349ca44e12ce1eb0ea445e0
|
7
|
+
data.tar.gz: ec33d942d728639ccb52052f317a5c7e3d66401296fa69a57f1856e81f044339ee62278c3680ce9d2fadc6d46c0bb38a9b51918edc8fb4a5fe54363283c9aa6f
|
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/
|
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
|
data/lib/zaikio/jwt_auth.rb
CHANGED
@@ -18,7 +18,7 @@ module Zaikio
|
|
18
18
|
def self.configure
|
19
19
|
self.configuration ||= Configuration.new
|
20
20
|
|
21
|
-
if Zaikio.const_defined?("Webhooks")
|
21
|
+
if Zaikio.const_defined?("Webhooks", false)
|
22
22
|
Zaikio::Webhooks.on "directory.revoked_access_token", Zaikio::JWTAuth::RevokeAccessTokenJob,
|
23
23
|
perform_now: true
|
24
24
|
end
|
@@ -52,6 +52,20 @@ module Zaikio
|
|
52
52
|
@mocked_jwt_payload = payload
|
53
53
|
end
|
54
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
|
+
|
55
69
|
module ClassMethods
|
56
70
|
def authorize_by_jwt_subject_type(type = nil)
|
57
71
|
@authorize_by_jwt_subject_type ||= type
|
@@ -68,9 +82,8 @@ module Zaikio
|
|
68
82
|
|
69
83
|
module InstanceMethods
|
70
84
|
def authenticate_by_jwt
|
71
|
-
|
72
|
-
|
73
|
-
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
|
74
87
|
|
75
88
|
return if show_error_if_token_is_revoked(token_data)
|
76
89
|
|
@@ -98,21 +111,6 @@ module Zaikio
|
|
98
111
|
|
99
112
|
private
|
100
113
|
|
101
|
-
def jwt_from_auth_header
|
102
|
-
return true if Zaikio::JWTAuth.mocked_jwt_payload
|
103
|
-
|
104
|
-
auth_header = request.headers["Authorization"]
|
105
|
-
auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
|
106
|
-
end
|
107
|
-
|
108
|
-
def jwt_payload
|
109
|
-
return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
|
110
|
-
|
111
|
-
payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
|
112
|
-
|
113
|
-
payload
|
114
|
-
end
|
115
|
-
|
116
114
|
def show_error_if_authorize_by_jwt_scopes_fails(token_data)
|
117
115
|
return if token_data.scope_by_configurations?(
|
118
116
|
self.class.authorize_by_jwt_scopes,
|
@@ -18,6 +18,7 @@ module Zaikio
|
|
18
18
|
def initialize
|
19
19
|
@environment = :sandbox
|
20
20
|
@revoked_token_ids = nil
|
21
|
+
@keys = nil
|
21
22
|
end
|
22
23
|
|
23
24
|
def logger
|
@@ -30,7 +31,7 @@ module Zaikio
|
|
30
31
|
end
|
31
32
|
|
32
33
|
def keys
|
33
|
-
|
34
|
+
@keys.is_a?(Proc) ? @keys.call : @keys
|
34
35
|
end
|
35
36
|
|
36
37
|
def revoked_token_ids
|
@@ -5,6 +5,8 @@ require "logger"
|
|
5
5
|
module Zaikio
|
6
6
|
module JWTAuth
|
7
7
|
class DirectoryCache
|
8
|
+
BadResponseError = Class.new(StandardError)
|
9
|
+
|
8
10
|
class << self
|
9
11
|
def fetch(directory_path, options = {})
|
10
12
|
cache = Zaikio::JWTAuth.configuration.redis.get("zaikio::jwt_auth::#{directory_path}")
|
@@ -48,10 +50,10 @@ module Zaikio
|
|
48
50
|
}.to_json)
|
49
51
|
|
50
52
|
data
|
51
|
-
rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
|
53
|
+
rescue Errno::ECONNREFUSED, Net::ReadTimeout, BadResponseError => e
|
52
54
|
raise unless (retries += 1) <= 3
|
53
55
|
|
54
|
-
Zaikio::JWTAuth.configuration.logger.
|
56
|
+
Zaikio::JWTAuth.configuration.logger.info("Timeout (#{e}), retrying in 1 second...")
|
55
57
|
sleep(1)
|
56
58
|
retry
|
57
59
|
end
|
@@ -59,7 +61,12 @@ module Zaikio
|
|
59
61
|
|
60
62
|
def fetch_from_directory(directory_path)
|
61
63
|
uri = URI("#{Zaikio::JWTAuth.configuration.host}/#{directory_path}")
|
62
|
-
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
response = http.request(Net::HTTP::Get.new(uri.request_uri))
|
66
|
+
raise BadResponseError unless (200..299).cover?(response.code.to_i)
|
67
|
+
raise BadResponseError unless response["content-type"].to_s.include?("application/json")
|
68
|
+
|
69
|
+
Oj.load(response.body)
|
63
70
|
end
|
64
71
|
end
|
65
72
|
end
|
@@ -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
|
@@ -38,8 +46,8 @@ module Zaikio
|
|
38
46
|
end
|
39
47
|
|
40
48
|
# scope_options is an array of objects with:
|
41
|
-
# scope, app_name (optional), except/only (array, optional)
|
42
|
-
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
|
43
51
|
configuration = scope_configurations.find do |scope_configuration|
|
44
52
|
action_matches = action_matches_config?(scope_configuration, action_name)
|
45
53
|
|
@@ -54,7 +62,7 @@ module Zaikio
|
|
54
62
|
|
55
63
|
return true unless configuration
|
56
64
|
|
57
|
-
scope?(configuration[:scopes], action_name, configuration[:app_name])
|
65
|
+
scope?(configuration[:scopes], action_name, app_name: configuration[:app_name], type: configuration[:type])
|
58
66
|
end
|
59
67
|
|
60
68
|
def action_matches_config?(scope_configuration, action_name)
|
@@ -67,14 +75,14 @@ module Zaikio
|
|
67
75
|
end
|
68
76
|
end
|
69
77
|
|
70
|
-
def scope?(allowed_scopes, action_name, app_name
|
78
|
+
def scope?(allowed_scopes, action_name, app_name: nil, type: nil)
|
71
79
|
app_name ||= Zaikio::JWTAuth.configuration.app_name
|
72
80
|
Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
|
73
81
|
scope.any? do |s|
|
74
82
|
parts = s.split(".")
|
75
83
|
parts[0] == app_name &&
|
76
84
|
parts[1] == allowed_scope &&
|
77
|
-
|
85
|
+
action_permitted?(action_name, parts[2], type: type)
|
78
86
|
end
|
79
87
|
end
|
80
88
|
end
|
@@ -101,8 +109,14 @@ module Zaikio
|
|
101
109
|
|
102
110
|
private
|
103
111
|
|
104
|
-
def
|
105
|
-
|
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
|
106
120
|
end
|
107
121
|
end
|
108
122
|
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.
|
4
|
+
version: 0.5.0
|
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: 2021-
|
13
|
+
date: 2021-04-21 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: oj
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
- !ruby/object:Gem::Version
|
28
28
|
version: 3.0.0
|
29
29
|
- !ruby/object:Gem::Dependency
|
30
|
-
name:
|
30
|
+
name: railties
|
31
31
|
requirement: !ruby/object:Gem::Requirement
|
32
32
|
requirements:
|
33
33
|
- - ">="
|
@@ -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://
|
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:
|
@@ -97,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
99
|
- !ruby/object:Gem::Version
|
98
100
|
version: '0'
|
99
101
|
requirements: []
|
100
|
-
rubygems_version: 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
|