token_manager 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +168 -7
- data/lib/token_manager/version.rb +1 -1
- data/lib/token_manager.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ff4a8194d49805843a8627838efc10e1d10775dec27f077c244469045bd4c07
|
4
|
+
data.tar.gz: 4eadf80e12f17624508f2dd00fb0ceaebcf937d72ed2e269f7f3cecf98b3ee77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c5aec9184aabf13de9c6030a8903a289bee5a5b0821cf3f716c0c63d15db4026b5413b9a39472fc718e6df284f66d7759ac43c638377055c3d669336be0f49a
|
7
|
+
data.tar.gz: 1dcd35575c52d77b95e4d25b948a4635954d281105faa5f0f5b97c478a476fc1dcd69b0798eaa34eaa3fe408589d826a2a1131ff320c8a226df1964da47f9f93
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,24 +1,185 @@
|
|
1
1
|
# TokenManager
|
2
2
|
|
3
|
-
|
3
|
+
`TokenManager` is designed to handle inter micro-service communication without sharing secrets between the services.
|
4
|
+
It uses asymmetric signature to verify the hosts.
|
4
5
|
|
5
|
-
|
6
|
+
The workflow schema looks next:
|
7
|
+
1. Service A generates a signed token and adds it to a request
|
8
|
+
2. Service B receives the request with the token, takes a public_key id (`kid`) from the token and requests Service A for the public key via http request
|
9
|
+
3. Service A responds with the public key
|
10
|
+
4. Service B verifies the token using the public_key
|
6
11
|
|
7
12
|
## Installation
|
8
13
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
14
|
Install the gem and add to the application's Gemfile by executing:
|
12
15
|
|
13
|
-
$ bundle add
|
16
|
+
$ bundle add token_manager
|
14
17
|
|
15
18
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
19
|
|
17
|
-
$ gem install
|
20
|
+
$ gem install token_manager
|
18
21
|
|
19
22
|
## Usage
|
20
23
|
|
21
|
-
|
24
|
+
### Basic configuration
|
25
|
+
|
26
|
+
Create classes that inherit from TokenManager.
|
27
|
+
|
28
|
+
You need to override `with_redis` method. It must `yield` the given block and
|
29
|
+
provide `Redis::Client` instance as an argument (the implementation depends on your redis config).
|
30
|
+
|
31
|
+
Also it can be useful to make a "factory" method that will return an instance of the token manager.
|
32
|
+
|
33
|
+
Token manager expects to receive next arguments:
|
34
|
+
|
35
|
+
* service_name (required) is a string that represents the current micro-service name. It will be used as `iss`
|
36
|
+
in the JWT. It must be in the `trusted_issuers` (see below) in the receiver's config to be able to verify during the decoding.
|
37
|
+
|
38
|
+
* trusted_issuers (optional) is a hash where the keys represent allowed issuer and value is a config to retrieve a public_key for
|
39
|
+
that issuer. TokenManager will send a GET request to the provided url with `kid` (key id) parameter. As a result it
|
40
|
+
expects a JSON response like { public_key: "...public_key_here..." }
|
41
|
+
|
42
|
+
* token_ttl (optional) will add an expiration claim to every encoded JWT (`exp: Time.now + token_ttl`). If the config
|
43
|
+
is skipped it will require to pass `exp` claim explicitly to every `encode` method call.
|
44
|
+
|
45
|
+
* public_key_ttl (default 1 month) not to retrieve the public_key each time the receiver caches it in Redis.
|
46
|
+
This is the Redis cache TTL
|
47
|
+
|
48
|
+
* old_key_ttl (default 1 week) after you regenerate the `private_key` its `public_key` still must be
|
49
|
+
stored to verify already generated tokens. This is the Redis cache TTL for the private and public keys.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
######### micro-service A
|
53
|
+
|
54
|
+
# app/models/a_token.rb
|
55
|
+
class AToken < TokenManager
|
56
|
+
def self.instance
|
57
|
+
@instance ||= new(
|
58
|
+
service_name: 'a_service',
|
59
|
+
token_ttl: 1.minute,
|
60
|
+
trusted_issuers: {
|
61
|
+
b_service: { url: 'http://localhost:3001/public_keys' }
|
62
|
+
}
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# uses redis for caching
|
67
|
+
private def with_redis
|
68
|
+
Rails.cache.redis.with { |redis| yield(redis) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# app/controllers/public_keys_controller
|
73
|
+
class PublicKeysController < ApplicationController
|
74
|
+
def index
|
75
|
+
return render(json: { error: '`kid` is required' }, status: 400) unless params[:kid]
|
76
|
+
|
77
|
+
public_key = AToken.instance.public_key(params[:kid])
|
78
|
+
return render(json: { error: 'public_keys not found' }, status: 404) unless public_key
|
79
|
+
|
80
|
+
render json: {
|
81
|
+
kid: params[:kid],
|
82
|
+
public_key: public_key,
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# config/routes.rb
|
87
|
+
get 'public_keys', to: 'public_keys#index'
|
88
|
+
|
89
|
+
######### micro-service B
|
90
|
+
|
91
|
+
# app/models/a_token.rb
|
92
|
+
class BToken < TokenManager
|
93
|
+
def self.instance
|
94
|
+
@instance ||= new(
|
95
|
+
service_name: 'b_service',
|
96
|
+
token_ttl: 10.minutes,
|
97
|
+
trusted_issuers: {
|
98
|
+
a_service: { url: 'https://localhost:3000/public_keys' }
|
99
|
+
}
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# uses redis for caching
|
104
|
+
private def with_redis
|
105
|
+
@redis ||= ::Redis.new
|
106
|
+
yield(@redis)
|
107
|
+
end
|
108
|
+
|
109
|
+
def retrieve_issuer_key(iss, kid)
|
110
|
+
AToken.instance.public_key(kid)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# app/controllers/public_keys_controller
|
115
|
+
class PublicKeysController < ApplicationController
|
116
|
+
def index
|
117
|
+
return render(json: { error: '`kid` is required' }, status: 400) unless params[:kid]
|
118
|
+
|
119
|
+
public_key = BToken.instance.public_key(params[:kid])
|
120
|
+
return render(json: { error: 'public_keys not found' }, status: 404) unless public_key
|
121
|
+
|
122
|
+
render json: {
|
123
|
+
kid: params[:kid],
|
124
|
+
public_key: public_key,
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# config/routes.rb
|
130
|
+
get 'public_keys', to: 'public_keys#index'
|
131
|
+
```
|
132
|
+
|
133
|
+
Now you need to run `rails s -p 3000` for service A and `rails s -p 3001` for service B in different terminals
|
134
|
+
so the services can retrieve public keys. Open services' consoles and try next:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# console A
|
138
|
+
token = AToken.instance.encode(aud: 'b_service', foo: 'bar') # "eyJraWQiOiJjNTcxNDRjYS04YWJhLTRlMWMtOGUwNC05YjZkYTc..."
|
139
|
+
# console B
|
140
|
+
BToken.instance.decode(token) # [{"exp"=>1679914355, "aud"=>"b_service", "foo"=>"bar", "iss"=>"a_service"}, {"kid"=>"c57144ca-8aba-4e1c-8e04-9b6da70a5dc6", "alg"=>"RS256"}]
|
141
|
+
```
|
142
|
+
As you can see the `encode` method requires you to specify `aud` claim with the destination service name. Also it adds
|
143
|
+
`iss` and `exp` claims (`exp` claim adds automatically only if `token_ttl` was specified).
|
144
|
+
|
145
|
+
### Helpers
|
146
|
+
|
147
|
+
#### Faraday middleware
|
148
|
+
|
149
|
+
If you use libraries which create a connection instance an reuse it you can face a problem that you can't just generate
|
150
|
+
a token and specify it in the connection because the token has its TTL. To solve it you can
|
151
|
+
use `TokenManager::FaradayMiddleware` that receives a block to generate tokens on the fly.
|
152
|
+
Here is an example of the middleware usage with JsonApiClient gem:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
ServiceB::Resources::Base.connection do |connection|
|
156
|
+
connection.use TokenManager::FaradayMiddleware do
|
157
|
+
AToken.instance.encode(aud: 'service_b')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
On the ServiceB side you can use `token_from` method to retrieve the token:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
class ApplicationController < ActionController::API
|
166
|
+
# ... code ...
|
167
|
+
before_action :authenticate
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def authenticate
|
172
|
+
raise(Unauthorized, 'not authorized') unless token['iss'] == 'service_a' # only service_a has access
|
173
|
+
end
|
174
|
+
|
175
|
+
def token
|
176
|
+
return @token if defined?(@token)
|
177
|
+
|
178
|
+
encoded_token = BToken.token_from(request.headers) || raise(Unauthorized, 'token is absent')
|
179
|
+
@token = BToken.instance.decode(encoded_token).first
|
180
|
+
end
|
181
|
+
end
|
182
|
+
```
|
22
183
|
|
23
184
|
## Development
|
24
185
|
|
data/lib/token_manager.rb
CHANGED
@@ -21,8 +21,8 @@ class TokenManager
|
|
21
21
|
@service_name = options['service_name'] || raise(ArgumentError, '`service_name` is required')
|
22
22
|
@trusted_issuers = options['trusted_issuers'] || {}
|
23
23
|
@token_ttl = options['token_ttl']
|
24
|
-
@public_key_ttl = options['public_key_ttl'] || 1.
|
25
|
-
@old_key_ttl = options['
|
24
|
+
@public_key_ttl = options['public_key_ttl'] || 1.month
|
25
|
+
@old_key_ttl = options['old_key_ttl'] || 1.week
|
26
26
|
end
|
27
27
|
|
28
28
|
def encode(payload)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: token_manager
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bogdan Guban
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|