zaikio-jwt_auth 0.1.3 → 0.2.0
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 +4 -4
- data/README.md +44 -31
- data/app/jobs/zaikio/jwt_auth/revoke_access_token_job.rb +12 -0
- data/lib/zaikio/jwt_auth.rb +44 -13
- data/lib/zaikio/jwt_auth/configuration.rb +1 -0
- data/lib/zaikio/jwt_auth/engine.rb +9 -0
- data/lib/zaikio/jwt_auth/test_helper.rb +23 -0
- data/lib/zaikio/jwt_auth/token_data.rb +30 -0
- data/lib/zaikio/jwt_auth/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 608b4341e5cb5797a302e65439882e050044fd2676b0fc01f7931783529ee032
|
4
|
+
data.tar.gz: 1dcaeb58daa1f352c352b8a3e65bab7e46618c4dba30f64af2141c25bb1f2dee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2289f1f2fc4ddc1a84070f6df75ebbfb143b4b0634ff8a64d3c54cfa5fb2741de8734aaa398989fd9fb63a43dde8ccb4849fa497eee8f6e3a1897acd5fde4dcf
|
7
|
+
data.tar.gz: 4b935055a6461f2f2e22dec109634e41b40222773fb363811e76d29327eea22d81210a470e6d9865092473d114c1c21646a99bcbb0942cdc4e39229f7f4e8291
|
data/README.md
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
Gem for JWT-Based authentication and authorization with zaikio.
|
4
4
|
|
5
|
-
## Usage
|
6
|
-
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
1. Add this line to your application's Gemfile:
|
7
|
+
### 1. Add this line to your application's Gemfile:
|
10
8
|
|
11
9
|
```ruby
|
12
10
|
gem 'zaikio-jwt_auth'
|
@@ -22,7 +20,7 @@ Or install it yourself as:
|
|
22
20
|
$ gem install zaikio-jwt_auth
|
23
21
|
```
|
24
22
|
|
25
|
-
2. Configure the gem:
|
23
|
+
### 2. Configure the gem:
|
26
24
|
|
27
25
|
```rb
|
28
26
|
# config/initializers/zaikio_jwt_auth.rb
|
@@ -34,7 +32,7 @@ Zaikio::JWTAuth.configure do |config|
|
|
34
32
|
end
|
35
33
|
```
|
36
34
|
|
37
|
-
3. Extend your API application controller:
|
35
|
+
### 3. Extend your API application controller:
|
38
36
|
|
39
37
|
```rb
|
40
38
|
class API::ApplicationController < ActionController::Base
|
@@ -49,44 +47,59 @@ class API::ApplicationController < ActionController::Base
|
|
49
47
|
end
|
50
48
|
```
|
51
49
|
|
52
|
-
4. Update Revoked Access Tokens by Webhook
|
50
|
+
### 4. Update Revoked Access Tokens by Webhook
|
51
|
+
|
52
|
+
This gem automatically registers a webhook, if you have properly setup [Zaikio::Webhooks](https://github.com/crispymtn/zaikio-webhooks).
|
53
|
+
|
54
|
+
|
55
|
+
### 5. Add more restrictions to your resources:
|
53
56
|
|
54
57
|
```rb
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
class API::ResourcesController < API::ApplicationController
|
59
|
+
authorize_by_jwt_subject_type 'Organization'
|
60
|
+
authorize_by_jwt_scopes 'resources'
|
61
|
+
end
|
62
|
+
```
|
60
63
|
|
61
|
-
|
62
|
-
before_action :update_blacklisted_access_tokens_by_webhook
|
64
|
+
### 6. Optionally, if you are using SSO: Check revoked tokens
|
63
65
|
|
64
|
-
|
65
|
-
case params[:name]
|
66
|
-
# Manage other events
|
67
|
-
end
|
68
|
-
end
|
66
|
+
Additionally, the API provides a method called `revoked_jwt?` which expects the `jti` of the JWT.
|
69
67
|
|
70
|
-
|
68
|
+
```rb
|
69
|
+
Zaikio::JWTAuth.revoked_jwt?('jti-of-token') # returns true if token was revoked
|
70
|
+
```
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
81
|
-
end
|
72
|
+
### 7. Optionally, use the test helper module to mock JWTs in your minitests
|
73
|
+
|
74
|
+
```rb
|
75
|
+
# in your test_helper.rb
|
76
|
+
include Zaikio::JWTAuth::TestHelper
|
77
|
+
|
78
|
+
# in your tests you can use:
|
79
|
+
mock_jwt(sub: 'Organization/123', scope: ['directory.organization.r'])
|
82
80
|
```
|
83
81
|
|
82
|
+
## Advanced
|
84
83
|
|
85
|
-
|
84
|
+
### `only` and `except`
|
85
|
+
|
86
|
+
Similar to Rails' controller callbacks, `authorize_by_jwt_scopes` can also be passed a list of actions:
|
86
87
|
|
87
88
|
```rb
|
88
89
|
class API::ResourcesController < API::ApplicationController
|
89
90
|
authorize_by_jwt_subject_type 'Organization'
|
90
|
-
authorize_by_jwt_scopes 'resources'
|
91
|
+
authorize_by_jwt_scopes 'resources', except: :destroy
|
92
|
+
authorize_by_jwt_scopes 'remove_resources', only: [:destroy]
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
|
97
|
+
### `if` and `unless`
|
98
|
+
|
99
|
+
Similar to Rails' controller callbacks, `authorize_by_jwt_scopes` can also handle a lambda in the context of the controller to request parameters.
|
100
|
+
|
101
|
+
```rb
|
102
|
+
class API::ResourcesController < API::ApplicationController
|
103
|
+
authorize_by_jwt_scopes 'resources', unless: -> { params[:skip] == '1' }
|
91
104
|
end
|
92
105
|
```
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module JWTAuth
|
3
|
+
class RevokeAccessTokenJob < ApplicationJob
|
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"]
|
7
|
+
data
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/zaikio/jwt_auth.rb
CHANGED
@@ -5,6 +5,8 @@ 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/engine"
|
9
|
+
require "zaikio/jwt_auth/test_helper"
|
8
10
|
|
9
11
|
module Zaikio
|
10
12
|
module JWTAuth
|
@@ -14,21 +16,51 @@ module Zaikio
|
|
14
16
|
|
15
17
|
def self.configure
|
16
18
|
self.configuration ||= Configuration.new
|
19
|
+
|
20
|
+
if Zaikio.const_defined?("Webhooks")
|
21
|
+
Zaikio::Webhooks.on "directory.revoked_access_token", Zaikio::JWTAuth::RevokeAccessTokenJob,
|
22
|
+
perform_now: true
|
23
|
+
end
|
24
|
+
|
17
25
|
yield(configuration)
|
18
26
|
end
|
19
27
|
|
28
|
+
def self.revoked_jwt?(jti)
|
29
|
+
blacklisted_token_ids.include?(jti)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.blacklisted_token_ids
|
33
|
+
return [] if mocked_jwt_payload
|
34
|
+
|
35
|
+
return configuration.blacklisted_token_ids if configuration.blacklisted_token_ids
|
36
|
+
|
37
|
+
DirectoryCache.fetch("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes)["blacklisted_token_ids"]
|
38
|
+
end
|
39
|
+
|
20
40
|
def self.included(base)
|
21
41
|
base.send :include, InstanceMethods
|
22
42
|
base.send :extend, ClassMethods
|
23
43
|
end
|
24
44
|
|
45
|
+
def self.mocked_jwt_payload
|
46
|
+
@mocked_jwt_payload
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.mocked_jwt_payload=(payload)
|
50
|
+
@mocked_jwt_payload = payload
|
51
|
+
end
|
52
|
+
|
25
53
|
module ClassMethods
|
26
54
|
def authorize_by_jwt_subject_type(type = nil)
|
27
55
|
@authorize_by_jwt_subject_type ||= type
|
28
56
|
end
|
29
57
|
|
30
58
|
def authorize_by_jwt_scopes(scopes = nil, options = {})
|
31
|
-
@authorize_by_jwt_scopes ||=
|
59
|
+
@authorize_by_jwt_scopes ||= []
|
60
|
+
|
61
|
+
@authorize_by_jwt_scopes << options.merge(scopes: scopes) if scopes
|
62
|
+
|
63
|
+
@authorize_by_jwt_scopes
|
32
64
|
end
|
33
65
|
end
|
34
66
|
|
@@ -54,7 +86,7 @@ module Zaikio
|
|
54
86
|
def update_blacklisted_access_tokens_by_webhook
|
55
87
|
return unless params[:name] == "directory.revoked_access_token"
|
56
88
|
|
57
|
-
DirectoryCache.update("api/v1/
|
89
|
+
DirectoryCache.update("api/v1/blacklisted_access_tokens.json", expires_after: 60.minutes) do |data|
|
58
90
|
data["blacklisted_token_ids"] << params[:payload][:access_token_id]
|
59
91
|
data
|
60
92
|
end
|
@@ -65,19 +97,26 @@ module Zaikio
|
|
65
97
|
private
|
66
98
|
|
67
99
|
def jwt_from_auth_header
|
100
|
+
return true if Zaikio::JWTAuth.mocked_jwt_payload
|
101
|
+
|
68
102
|
auth_header = request.headers["Authorization"]
|
69
103
|
auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
|
70
104
|
end
|
71
105
|
|
72
106
|
def jwt_payload
|
107
|
+
return Zaikio::JWTAuth.mocked_jwt_payload if Zaikio::JWTAuth.mocked_jwt_payload
|
108
|
+
|
73
109
|
payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
|
74
110
|
|
75
111
|
payload
|
76
112
|
end
|
77
113
|
|
78
114
|
def show_error_if_authorize_by_jwt_scopes_fails(token_data)
|
79
|
-
|
80
|
-
|
115
|
+
return if token_data.scope_by_configurations?(
|
116
|
+
self.class.authorize_by_jwt_scopes,
|
117
|
+
action_name,
|
118
|
+
self
|
119
|
+
)
|
81
120
|
|
82
121
|
render_error("unpermitted_scope")
|
83
122
|
end
|
@@ -92,19 +131,11 @@ module Zaikio
|
|
92
131
|
end
|
93
132
|
|
94
133
|
def show_error_if_token_is_blacklisted(token_data)
|
95
|
-
return unless
|
134
|
+
return unless Zaikio::JWTAuth.revoked_jwt?(token_data.jti)
|
96
135
|
|
97
136
|
render_error("invalid_jwt")
|
98
137
|
end
|
99
138
|
|
100
|
-
def blacklisted_token_ids
|
101
|
-
if Zaikio::JWTAuth.configuration.blacklisted_token_ids
|
102
|
-
return Zaikio::JWTAuth.configuration.blacklisted_token_ids
|
103
|
-
end
|
104
|
-
|
105
|
-
DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes)["blacklisted_token_ids"]
|
106
|
-
end
|
107
|
-
|
108
139
|
def render_error(error, status: :forbidden)
|
109
140
|
render(status: status, json: { "errors" => [error] })
|
110
141
|
end
|
@@ -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
|
@@ -33,6 +33,36 @@ module Zaikio
|
|
33
33
|
@payload["jti"]
|
34
34
|
end
|
35
35
|
|
36
|
+
# 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)
|
39
|
+
configuration = scope_configurations.find do |scope_configuration|
|
40
|
+
action_matches = action_matches_config?(scope_configuration, action_name)
|
41
|
+
|
42
|
+
if action_matches && scope_configuration[:if] && !context.instance_exec(&scope_configuration[:if])
|
43
|
+
false
|
44
|
+
elsif action_matches && scope_configuration[:unless] && context.instance_exec(&scope_configuration[:unless])
|
45
|
+
false
|
46
|
+
else
|
47
|
+
action_matches
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return true unless configuration
|
52
|
+
|
53
|
+
scope?(configuration[:scopes], action_name, configuration[:app_name])
|
54
|
+
end
|
55
|
+
|
56
|
+
def action_matches_config?(scope_configuration, action_name)
|
57
|
+
if scope_configuration[:only]
|
58
|
+
Array(scope_configuration[:only]).any? { |a| a.to_s == action_name }
|
59
|
+
elsif scope_configuration[:except]
|
60
|
+
Array(scope_configuration[:except]).none? { |a| a.to_s == action_name }
|
61
|
+
else
|
62
|
+
true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
36
66
|
def scope?(allowed_scopes, action_name, app_name = nil)
|
37
67
|
app_name ||= Zaikio::JWTAuth.configuration.app_name
|
38
68
|
Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
|
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.
|
4
|
+
version: 0.2.0
|
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-
|
11
|
+
date: 2020-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 6.0.
|
33
|
+
version: 6.0.2.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 6.0.
|
40
|
+
version: 6.0.2.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: jwt
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,12 +62,15 @@ files:
|
|
62
62
|
- MIT-LICENSE
|
63
63
|
- README.md
|
64
64
|
- Rakefile
|
65
|
+
- app/jobs/zaikio/jwt_auth/revoke_access_token_job.rb
|
65
66
|
- lib/tasks/zaikio/jwt_auth_tasks.rake
|
66
67
|
- lib/zaikio/jwt_auth.rb
|
67
68
|
- lib/zaikio/jwt_auth/configuration.rb
|
68
69
|
- lib/zaikio/jwt_auth/directory_cache.rb
|
70
|
+
- lib/zaikio/jwt_auth/engine.rb
|
69
71
|
- lib/zaikio/jwt_auth/jwk.rb
|
70
72
|
- lib/zaikio/jwt_auth/railtie.rb
|
73
|
+
- lib/zaikio/jwt_auth/test_helper.rb
|
71
74
|
- lib/zaikio/jwt_auth/token_data.rb
|
72
75
|
- lib/zaikio/jwt_auth/version.rb
|
73
76
|
homepage: https://www.zaikio.com/
|