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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1d30fc81116b96e15351a9208a870ddeab36d7c0c157b8375afd1d8f3100d47
4
- data.tar.gz: 230a7ae244484bba33131dcf3cfed33713d3375b3f7813f1f90f7e30ac16eeee
3
+ metadata.gz: 0ff4a8194d49805843a8627838efc10e1d10775dec27f077c244469045bd4c07
4
+ data.tar.gz: 4eadf80e12f17624508f2dd00fb0ceaebcf937d72ed2e269f7f3cecf98b3ee77
5
5
  SHA512:
6
- metadata.gz: c947a400ec247ff1003eb5c62ebea2464f07fc1077a28bf1329bd68c5c4c3f17c60b32a84cf0270226bed6137515fb944d43d5607339bc8def0eda94b732bf23
7
- data.tar.gz: 3a76374f68ea875a88af3f11d9c590999dc50ff572fcd66fdaebb7d0b57ced8f8e0b42e33db70ab9d9bae03376bb5bbc2f84e7310e9ea40592b9bebe84448efd
6
+ metadata.gz: 1c5aec9184aabf13de9c6030a8903a289bee5a5b0821cf3f716c0c63d15db4026b5413b9a39472fc718e6df284f66d7759ac43c638377055c3d669336be0f49a
7
+ data.tar.gz: 1dcd35575c52d77b95e4d25b948a4635954d281105faa5f0f5b97c478a476fc1dcd69b0798eaa34eaa3fe408589d826a2a1131ff320c8a226df1964da47f9f93
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- token_manager (0.1.0)
4
+ token_manager (0.1.1)
5
5
  activesupport
6
6
  curb
7
7
  jwt (~> 2.0)
data/README.md CHANGED
@@ -1,24 +1,185 @@
1
1
  # TokenManager
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/token_manager`. To experiment with that code, run `bin/console` for an interactive prompt.
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 UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
20
+ $ gem install token_manager
18
21
 
19
22
  ## Usage
20
23
 
21
- TODO: Write usage instructions here
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TokenManager
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
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.day
25
- @old_key_ttl = options['public_key_ttl'] || 1.month
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.0
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-23 00:00:00.000000000 Z
11
+ date: 2023-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport