hydra-keycloak-client 0.1.13 → 0.1.14

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: 6b9fb72f474785e45fca86279c8a7877f73605bf3da22507a801ef7458f107b1
4
- data.tar.gz: cc12951887ddd3f14aa662788b36c06d41095934707be9a4fe1ba685b8b1afa1
3
+ metadata.gz: 0df63eb8a97b5a549f867fa0de5fe449c9791ca811195c1aba5610fb54a278e6
4
+ data.tar.gz: 05d90289f5a8dce6618664a44ba1e7f0b1cd4d901b09895d52b43ef7fac8666c
5
5
  SHA512:
6
- metadata.gz: 5b026319391803ea1da1bee4bd47eb019a6c99ca48abd4a330353cdf55285378d1ef7d57f38d42520b59499962bbc393e6d17a1ed4b78e18087dc8e892d69ab5
7
- data.tar.gz: 2bf4d76068843e4e82bb65c86653b04ed506d373f14990b77991f8c1064f1e193967c39a265536b364228bf1afa6bd4d2ad77f74201488f0d0d7e124c5c130b0
6
+ metadata.gz: e00caef745d78165d825b25a69b7afcb578dda19e9bccd543a56c3640ad5be1f9d70e402d977d38594821c4d2a9a988f32dec05cdc5748dcdf58b834b090afbe
7
+ data.tar.gz: 919a92716cbf26f5e8f94b973fe5023d350c298bd541caa50e81d4ec824b59c789fd0fa52480d65e9c98e8405d92317b19090467102815725abbc62c42ba99cd
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  .rspec_status
2
+ coverage
3
+ .bundle
data/Gemfile CHANGED
@@ -11,5 +11,7 @@ gem 'rubocop', '~> 1.26'
11
11
 
12
12
  gem 'pry'
13
13
 
14
+ gem 'dalli', require: false, group: :test
15
+ gem 'redis', require: false, group: :test
14
16
  gem 'simplecov', require: false, group: :test
15
17
  gem 'simplecov-cobertura', require: false, group: :test
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hydra-keycloak-client (0.1.12)
4
+ hydra-keycloak-client (0.1.14)
5
5
  dry-auto_inject
6
6
  dry-container
7
7
  dry-monads
@@ -15,6 +15,8 @@ GEM
15
15
  ast (2.4.2)
16
16
  coderay (1.1.3)
17
17
  concurrent-ruby (1.1.10)
18
+ connection_pool (2.3.0)
19
+ dalli (3.2.3)
18
20
  diff-lcs (1.4.4)
19
21
  docile (1.4.0)
20
22
  dry-auto_inject (0.9.0)
@@ -22,12 +24,11 @@ GEM
22
24
  dry-configurable (0.15.0)
23
25
  concurrent-ruby (~> 1.0)
24
26
  dry-core (~> 0.6)
25
- dry-container (0.9.0)
27
+ dry-container (0.10.1)
26
28
  concurrent-ruby (~> 1.0)
27
- dry-configurable (~> 0.13, >= 0.13.0)
28
- dry-core (0.7.1)
29
+ dry-core (0.8.1)
29
30
  concurrent-ruby (~> 1.0)
30
- dry-inflector (0.2.1)
31
+ dry-inflector (0.3.0)
31
32
  dry-initializer (3.1.1)
32
33
  dry-logic (1.2.0)
33
34
  concurrent-ruby (~> 1.0)
@@ -63,6 +64,10 @@ GEM
63
64
  method_source (~> 1.0)
64
65
  rainbow (3.1.1)
65
66
  rake (12.3.3)
67
+ redis (5.0.5)
68
+ redis-client (>= 0.9.0)
69
+ redis-client (0.11.2)
70
+ connection_pool
66
71
  regexp_parser (2.2.1)
67
72
  rexml (3.2.5)
68
73
  rspec (3.10.0)
@@ -105,13 +110,15 @@ PLATFORMS
105
110
  ruby
106
111
 
107
112
  DEPENDENCIES
113
+ dalli
108
114
  hydra-keycloak-client!
109
115
  pry
110
116
  rake (~> 12.0)
117
+ redis
111
118
  rspec (~> 3.0)
112
119
  rubocop (~> 1.26)
113
120
  simplecov
114
121
  simplecov-cobertura
115
122
 
116
123
  BUNDLED WITH
117
- 2.2.25
124
+ 2.3.5
@@ -12,16 +12,39 @@ module Hydra
12
12
  module Keycloak
13
13
  class ConfigurationError < ::StandardError; end
14
14
 
15
+ MEMCACHED_SCHEMA = Dry::Schema.JSON do
16
+ required(:memcached_host).filled(:string)
17
+ required(:memcached_port).filled(:integer)
18
+ required(:memcached_namespace).filled(:string)
19
+ end
20
+
21
+ REDIS_SCHEMA = Dry::Schema.JSON do
22
+ required(:redis_host).filled(:string)
23
+ required(:redis_port).filled(:integer)
24
+ end
25
+
26
+ CONFIG_SCHEMA = Dry::Schema.JSON do
27
+ required(:auth_server_url).filled(:string)
28
+ required(:realm).filled(:string)
29
+ required(:client_id).filled(:string)
30
+ required(:redirect_uri).filled(:string)
31
+ required(:secret).filled(:string)
32
+ required(:logout_redirect).filled(:string)
33
+ required(:store_client).value(included_in?: %w[redis memcached])
34
+ required(:store_client_options).hash(MEMCACHED_SCHEMA | REDIS_SCHEMA)
35
+ optional(:scope).array(:str?)
36
+ end
37
+
15
38
  class ClientCreator
16
39
  extend ::Hydra::Keycloak::Mixin
17
40
 
18
41
  class << self
19
42
  def call(config:)
20
- register_containers(validate_config(config))
43
+ register_containers(validate_config(config).to_h)
21
44
  end
22
45
 
23
46
  def validate_config(config)
24
- validated_config = config_schema.call(config)
47
+ validated_config = CONFIG_SCHEMA.call(config)
25
48
 
26
49
  if validated_config.failure?
27
50
  raise ConfigurationError, "Wrong configuration params: #{validated_config.errors(full: true).to_h}"
@@ -30,40 +53,8 @@ module Hydra
30
53
  validated_config
31
54
  end
32
55
 
33
- def config_schema
34
- memcached_schema = ::Hydra::Keycloak::ClientCreator.memcached_schema
35
- redis_schema = ::Hydra::Keycloak::ClientCreator.redis_schema
36
- Dry::Schema.JSON do
37
- required(:auth_server_url).filled(:string)
38
- required(:realm).filled(:string)
39
- required(:client_id).filled(:string)
40
- required(:redirect_uri).filled(:string)
41
- required(:secret).filled(:string)
42
- required(:logout_redirect).filled(:string)
43
- required(:store_client).value(included_in?: %w[redis memcached])
44
- required(:store_client_options).hash(memcached_schema | redis_schema)
45
- optional(:scope).array(:str?)
46
- end
47
- end
48
-
49
- def memcached_schema
50
- Dry::Schema.JSON do
51
- required(:memcached_host).filled(:string)
52
- required(:memcached_port).filled(:integer)
53
- required(:memcached_namespace).filled(:string)
54
- end
55
- end
56
-
57
- def redis_schema
58
- Dry::Schema.JSON do
59
- required(:redis_host).filled(:string)
60
- required(:redis_port).filled(:integer)
61
- end
62
- end
63
-
64
56
  def register_containers(validated_config)
65
57
  register_urls(validated_config)
66
- register_queries
67
58
  register_store_client(validated_config)
68
59
  register_store
69
60
  register_code_verifier
@@ -79,42 +70,43 @@ module Hydra
79
70
  end
80
71
  end
81
72
 
82
- def register_queries
83
- container.register :queries do
84
- require 'hydra/keycloak/queries/gateway'
73
+ def register_store_client(config)
74
+ case config.fetch(:store_client)
75
+ when 'redis'
76
+ register_redis_store(config.fetch(:store_client_options))
77
+ when 'memcached'
78
+ register_memcached_store(config.fetch(:store_client_options))
79
+ end
80
+ end
81
+
82
+ def register_redis_store(redis_host:, redis_port:)
83
+ require 'hydra/keycloak/store/adapters/redis'
85
84
 
86
- ::Hydra::Keycloak::Queries::Gateway.new
85
+ container.register :redis do
86
+ ::Redis.new(host: redis_host, port: redis_port)
87
+ end
88
+
89
+ container.register :store_client do
90
+ require 'hydra/keycloak/store/redis_client'
91
+
92
+ ::Hydra::Keycloak::Store::RedisClient.new
87
93
  end
88
94
  end
89
95
 
90
- def register_store_client(config)
91
- case config[:store_client]
92
- when 'redis'
93
- require 'hydra/keycloak/store/adapters/redis'
96
+ def register_memcached_store(memcached_host:, memcached_port:, memcached_namespace:)
97
+ require 'hydra/keycloak/store/adapters/memcached'
94
98
 
95
- container.register :redis do
96
- ::Redis.new(host: config[:redis_host], port: config[:redis_port])
97
- end
99
+ container.register :dalli do
100
+ ::Dalli::Client.new(
101
+ "#{memcached_host}:#{memcached_port}",
102
+ namespace: memcached_namespace
103
+ )
104
+ end
98
105
 
99
- container.register :store_client do
100
- require 'hydra/keycloak/store/redis_client'
106
+ container.register :store_client do
107
+ require 'hydra/keycloak/store/memcached_client'
101
108
 
102
- ::Hydra::Keycloak::Store::RedisClient.new
103
- end
104
- when 'memcached'
105
- require 'hydra/keycloak/store/adapters/memcached'
106
- container.register :dalli do
107
- ::Dalli::Client.new(
108
- "#{config[:store_client_options][:memcached_host]}:#{config[:store_client_options][:memcached_port]}",
109
- namespace: config[:store_client_options][:memcached_namespace]
110
- )
111
- end
112
-
113
- container.register :store_client do
114
- require 'hydra/keycloak/store/memcached_client'
115
-
116
- ::Hydra::Keycloak::Store::MemcachedClient.new
117
- end
109
+ ::Hydra::Keycloak::Store::MemcachedClient.new
118
110
  end
119
111
  end
120
112
 
@@ -135,7 +127,7 @@ module Hydra
135
127
  class Client
136
128
  extend ::Hydra::Keycloak::Mixin
137
129
  include ::Dry::Monads[:result, :do]
138
- inject['urls', 'queries', 'store', 'code_verifier']
130
+ inject['urls', 'tokens_repo', 'store', 'code_verifier']
139
131
 
140
132
  def auth_url
141
133
  code_verifier.generate
@@ -143,9 +135,23 @@ module Hydra
143
135
  end
144
136
 
145
137
  def authenticate!(auth_code)
146
- return Failure(status: 400, code: :auth_code_was_not_received) unless auth_code
138
+ tokens_repo.get_tokens(auth_code, code_verifier.value).fmap do |tokens|
139
+ access_token = tokens[:access_token]
140
+ id_token = tokens[:id_token]
141
+ refresh_token = tokens[:refresh_token]
142
+
143
+ session_state = access_token.session_state
144
+
145
+ save_token(session_state, 'access_token', access_token)
146
+ save_token(session_state, 'id_token', id_token)
147
+ save_token(session_state, 'refresh_token', refresh_token)
148
+
149
+ session_state
150
+ end
151
+ end
147
152
 
148
- queries.get_tokens(auth_code, code_verifier.value).fmap do |tokens|
153
+ def authenticate_by_password!(username, password)
154
+ tokens_repo.get_tokens_by_password(username, password).fmap do |tokens|
149
155
  access_token = tokens[:access_token]
150
156
  id_token = tokens[:id_token]
151
157
  refresh_token = tokens[:refresh_token]
@@ -180,7 +186,7 @@ module Hydra
180
186
  access_token = yield fetch_token(session_state, 'access_token')
181
187
  end
182
188
 
183
- queries.token_introspect(access_token.source)
189
+ tokens_repo.introspect_token(access_token.source)
184
190
  end
185
191
 
186
192
  def access_token_jti(session_state)
@@ -198,7 +204,7 @@ module Hydra
198
204
  end
199
205
 
200
206
  def introspect_token(token)
201
- queries.token_introspect(token)
207
+ tokens_repo.introspect_token(token)
202
208
  end
203
209
 
204
210
  private
@@ -229,7 +235,7 @@ module Hydra
229
235
 
230
236
  def refresh_tokens(session_state)
231
237
  refresh_token = yield fetch_token(session_state, 'refresh_token')
232
- new_tokens = yield queries.refresh_tokens(refresh_token.source)
238
+ new_tokens = yield tokens_repo.refresh_tokens(refresh_token.source)
233
239
 
234
240
  yield save_token(session_state, 'access_token', new_tokens[:access_token])
235
241
  yield save_token(session_state, 'id_token', new_tokens[:id_token])
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
3
4
  require 'digest'
4
5
  require 'securerandom'
5
6
 
@@ -26,7 +27,7 @@ module Hydra
26
27
 
27
28
  def _generate_pkce(code_verifier)
28
29
  # https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
29
- Digest::SHA256.base64digest(code_verifier).tr('+/', '-_').tr('=', '')
30
+ Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
30
31
  end
31
32
  end
32
33
  end
@@ -8,10 +8,22 @@ module Hydra
8
8
  class Container
9
9
  extend Dry::Container::Mixin
10
10
 
11
- register(:http_client) do
12
- require 'hydra/keycloak/queries/http_client'
11
+ register(:http) do
12
+ require 'net/http'
13
13
 
14
- ::Hydra::Keycloak::Queries::HttpClient.new
14
+ Net::HTTP
15
+ end
16
+
17
+ register(:tokens_gateway) do
18
+ require 'hydra/keycloak/tokens/gateway'
19
+
20
+ ::Hydra::Keycloak::Tokens::Gateway.new
21
+ end
22
+
23
+ register(:tokens_repo) do
24
+ require 'hydra/keycloak/tokens/repo'
25
+
26
+ ::Hydra::Keycloak::Tokens::Repo.new
15
27
  end
16
28
  end
17
29
 
@@ -3,13 +3,18 @@
3
3
  require 'net/http'
4
4
  require 'json'
5
5
  require 'dry/monads'
6
+ require 'hydra/keycloak/container'
6
7
 
7
8
  module Hydra
8
9
  module Keycloak
9
- module Queries
10
- class HttpClient
10
+ module Tokens
11
+ class Gateway
12
+ extend ::Hydra::Keycloak::Mixin
13
+
11
14
  include ::Dry::Monads[:result]
12
15
 
16
+ inject['http']
17
+
13
18
  NETWORK_ERRORS = [Timeout::Error,
14
19
  Errno::EINVAL,
15
20
  Errno::ECONNRESET,
@@ -19,8 +24,8 @@ module Hydra
19
24
  Net::HTTPHeaderSyntaxError,
20
25
  Net::ProtocolError].freeze
21
26
 
22
- def do_post_request(path, body)
23
- response = Net::HTTP.post_form(URI(path), **body)
27
+ def post(path, body)
28
+ response = http.post_form(URI(path), **body)
24
29
 
25
30
  if response.code == '200'
26
31
  json = JSON.parse(response.body)
@@ -7,15 +7,19 @@ require 'hydra/keycloak/token'
7
7
 
8
8
  module Hydra
9
9
  module Keycloak
10
- module Queries
11
- class Gateway
10
+ module Tokens
11
+ class Repo
12
12
  extend ::Hydra::Keycloak::Mixin
13
13
  include ::Dry::Monads[:result]
14
- inject['http_client', 'urls']
14
+ inject['tokens_gateway', 'urls']
15
15
 
16
16
  def get_tokens(auth_code, code_verifier)
17
- result = make_request(urls.token_endpoint,
18
- urls.token_request_body(auth_code, code_verifier))
17
+ return Failure(status: 400, code: :auth_code_was_not_received) unless auth_code
18
+
19
+ result = tokens_gateway.post(
20
+ urls.token_endpoint,
21
+ urls.auth_code_token_request_body(auth_code, code_verifier)
22
+ )
19
23
 
20
24
  result.fmap do |tokens|
21
25
  {
@@ -26,9 +30,28 @@ module Hydra
26
30
  end
27
31
  end
28
32
 
29
- def token_introspect(token)
30
- make_request(urls.introspection_endpoint,
31
- urls.introspection_request_body(token)).bind do |result|
33
+ def get_tokens_by_password(username, password)
34
+ return Failure(status: 400, code: :username_or_password_is_empty) if username.nil? || password.nil?
35
+
36
+ result = tokens_gateway.post(
37
+ urls.token_endpoint,
38
+ urls.password_token_request_body(username, password)
39
+ )
40
+
41
+ result.fmap do |tokens|
42
+ {
43
+ access_token: ::Hydra::Keycloak::Token.new(tokens['access_token']),
44
+ id_token: ::Hydra::Keycloak::Token.new(tokens['id_token']),
45
+ refresh_token: ::Hydra::Keycloak::Token.new(tokens['refresh_token'])
46
+ }
47
+ end
48
+ end
49
+
50
+ def introspect_token(token)
51
+ tokens_gateway.post(
52
+ urls.introspection_endpoint,
53
+ urls.introspection_request_body(token)
54
+ ).bind do |result|
32
55
  if result['active']
33
56
  Success(result)
34
57
  else
@@ -38,8 +61,10 @@ module Hydra
38
61
  end
39
62
 
40
63
  def refresh_tokens(refresh_token)
41
- make_request(urls.token_endpoint,
42
- urls.refresh_request_body(refresh_token)).bind do |result|
64
+ tokens_gateway.post(
65
+ urls.token_endpoint,
66
+ urls.refresh_request_body(refresh_token)
67
+ ).bind do |result|
43
68
  if result['error']
44
69
  Failure(status: 400, code: :token_refreshing_error)
45
70
  else
@@ -51,12 +76,6 @@ module Hydra
51
76
  end
52
77
  end
53
78
  end
54
-
55
- private
56
-
57
- def make_request(path, body)
58
- http_client.do_post_request(path, body)
59
- end
60
79
  end
61
80
  end
62
81
  end
@@ -10,22 +10,26 @@ module Hydra
10
10
  end
11
11
 
12
12
  def auth_url(code_challenge)
13
- "#{@config[:auth_server_url]}" \
14
- "realms/#{@config[:realm]}/protocol/openid-connect/auth/?" \
15
- 'response_type=code&' \
16
- "client_id=#{@config[:client_id]}&" \
17
- "redirect_uri=#{@config[:redirect_uri]}&" \
18
- "nonce=#{@config[:secret]}&" \
19
- "scope=#{scope}&" \
20
- "code_challenge=#{code_challenge}&" \
21
- 'code_challenge_method=S256'
13
+ URI(URI.join(@config[:auth_server_url], "realms/#{@config[:realm]}/protocol/openid-connect/auth")).tap do |uri|
14
+ uri.query = URI.encode_www_form(
15
+ {
16
+ response_type: 'code',
17
+ client_id: @config[:client_id],
18
+ redirect_uri: @config[:redirect_uri],
19
+ nonce: @config[:secret],
20
+ scope: scope,
21
+ code_challenge: code_challenge,
22
+ code_challenge_method: 'S256'
23
+ }
24
+ )
25
+ end.to_s
22
26
  end
23
27
 
24
28
  def token_endpoint
25
- "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/token"
29
+ URI.join(@config[:auth_server_url], "realms/#{@config[:realm]}/protocol/openid-connect/token")
26
30
  end
27
31
 
28
- def token_request_body(auth_code, code_verifier)
32
+ def auth_code_token_request_body(auth_code, code_verifier)
29
33
  {
30
34
  grant_type: 'authorization_code',
31
35
  code: auth_code,
@@ -36,8 +40,19 @@ module Hydra
36
40
  }
37
41
  end
38
42
 
43
+ def password_token_request_body(username, password)
44
+ {
45
+ grant_type: 'password',
46
+ username: username,
47
+ password: password,
48
+ scope: scope,
49
+ client_id: @config[:client_id],
50
+ client_secret: @config[:secret]
51
+ }
52
+ end
53
+
39
54
  def introspection_endpoint
40
- "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/token/introspect"
55
+ URI.join(@config[:auth_server_url], "realms/#{@config[:realm]}/protocol/openid-connect/token/introspect")
41
56
  end
42
57
 
43
58
  def introspection_request_body(token)
@@ -50,9 +65,9 @@ module Hydra
50
65
  end
51
66
 
52
67
  def end_session_url(id_token)
53
- "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/logout" \
54
- "?id_token_hint=#{id_token}" \
55
- "&post_logout_redirect_uri=#{@config[:logout_redirect]}"
68
+ URI.join(@config[:auth_server_url], "realms/#{@config[:realm]}/protocol/openid-connect/logout").tap do |uri|
69
+ uri.query = URI.encode_www_form(id_token_hint: id_token, post_logout_redirect_uri: @config[:logout_redirect])
70
+ end.to_s
56
71
  end
57
72
 
58
73
  def refresh_request_body(refresh_token)
@@ -68,7 +83,7 @@ module Hydra
68
83
  private
69
84
 
70
85
  def scope
71
- (DEFAULT_SCOPE + @config[:scope]).join('%20')
86
+ [*DEFAULT_SCOPE, *@config[:scope]].join(' ')
72
87
  end
73
88
  end
74
89
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hydra
4
4
  module Keycloak
5
- VERSION = '0.1.13'
5
+ VERSION = '0.1.14'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hydra-keycloak-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fedor Kosolapov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-31 00:00:00.000000000 Z
11
+ date: 2022-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -118,14 +118,14 @@ files:
118
118
  - lib/hydra/keycloak/client.rb
119
119
  - lib/hydra/keycloak/code_verifier.rb
120
120
  - lib/hydra/keycloak/container.rb
121
- - lib/hydra/keycloak/queries/gateway.rb
122
- - lib/hydra/keycloak/queries/http_client.rb
123
121
  - lib/hydra/keycloak/store/adapters/memcached.rb
124
122
  - lib/hydra/keycloak/store/adapters/redis.rb
125
123
  - lib/hydra/keycloak/store/gateway.rb
126
124
  - lib/hydra/keycloak/store/memcached_client.rb
127
125
  - lib/hydra/keycloak/store/redis_client.rb
128
126
  - lib/hydra/keycloak/token.rb
127
+ - lib/hydra/keycloak/tokens/gateway.rb
128
+ - lib/hydra/keycloak/tokens/repo.rb
129
129
  - lib/hydra/keycloak/urls.rb
130
130
  - lib/hydra/keycloak/version.rb
131
131
  - run_tests.sh
@@ -136,7 +136,7 @@ metadata:
136
136
  homepage_uri: https://github.com/hydra-billing/hydra-keycloak-client
137
137
  source_code_uri: https://github.com/hydra-billing/hydra-keycloak-client
138
138
  changelog_uri: https://github.com/hydra-billing/hydra-keycloak-client
139
- post_install_message:
139
+ post_install_message:
140
140
  rdoc_options: []
141
141
  require_paths:
142
142
  - lib
@@ -152,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  version: '0'
153
153
  requirements: []
154
154
  rubygems_version: 3.1.6
155
- signing_key:
155
+ signing_key:
156
156
  specification_version: 4
157
157
  summary: Keycloak client for SSO
158
158
  test_files: []