hydra-keycloak-client 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c0702045813eaac89c7c6de7629d3b1df9704e230491c7ad138f1c9bab5f2b0c
4
+ data.tar.gz: 0b40b85cb591c1f8a85d1689ce7d78d471c77053826604632b8fae0e90d37977
5
+ SHA512:
6
+ metadata.gz: 7e297eeabbb1a26cfaa4299b9a76b963838eec4ef1e71ba7ab9d9195dd994d46db4eec80d2d8e525e3bbeed63e02ce11d7aaedb69e7bc309bab80acc3f760ae0
7
+ data.tar.gz: 19f1c1fd13474f31e040f6802dd8459f0ab3d0dffabc083de1f749ec5b50e53951fbb1f87b32401bec61434ba06f5c58d6cb225061de2509b80fab4ea0d363d6
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hydra-keycloak-client.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+
9
+ gem 'jwt'
10
+ gem 'dalli'
11
+ gem 'redis'
12
+
13
+ gem 'dry-monads'
14
+ gem 'dry-auto_inject'
15
+ gem 'dry-container'
16
+ gem 'dry-schema'
17
+ gem 'dry-struct'
18
+
19
+ gem 'pry'
data/Gemfile.lock ADDED
@@ -0,0 +1,88 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hydra-keycloak-client (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.3)
10
+ concurrent-ruby (1.1.9)
11
+ dalli (3.1.1)
12
+ diff-lcs (1.4.4)
13
+ dry-auto_inject (0.8.0)
14
+ dry-container (>= 0.3.4)
15
+ dry-configurable (0.13.0)
16
+ concurrent-ruby (~> 1.0)
17
+ dry-core (~> 0.6)
18
+ dry-container (0.9.0)
19
+ concurrent-ruby (~> 1.0)
20
+ dry-configurable (~> 0.13, >= 0.13.0)
21
+ dry-core (0.7.1)
22
+ concurrent-ruby (~> 1.0)
23
+ dry-inflector (0.2.1)
24
+ dry-initializer (3.0.4)
25
+ dry-logic (1.2.0)
26
+ concurrent-ruby (~> 1.0)
27
+ dry-core (~> 0.5, >= 0.5)
28
+ dry-monads (1.4.0)
29
+ concurrent-ruby (~> 1.0)
30
+ dry-core (~> 0.7)
31
+ dry-schema (1.8.0)
32
+ concurrent-ruby (~> 1.0)
33
+ dry-configurable (~> 0.13, >= 0.13.0)
34
+ dry-core (~> 0.5, >= 0.5)
35
+ dry-initializer (~> 3.0)
36
+ dry-logic (~> 1.0)
37
+ dry-types (~> 1.5)
38
+ dry-struct (1.4.0)
39
+ dry-core (~> 0.5, >= 0.5)
40
+ dry-types (~> 1.5)
41
+ ice_nine (~> 0.11)
42
+ dry-types (1.5.1)
43
+ concurrent-ruby (~> 1.0)
44
+ dry-container (~> 0.3)
45
+ dry-core (~> 0.5, >= 0.5)
46
+ dry-inflector (~> 0.1, >= 0.1.2)
47
+ dry-logic (~> 1.0, >= 1.0.2)
48
+ ice_nine (0.11.2)
49
+ jwt (2.3.0)
50
+ method_source (1.0.0)
51
+ pry (0.14.1)
52
+ coderay (~> 1.1)
53
+ method_source (~> 1.0)
54
+ rake (12.3.3)
55
+ redis (4.6.0)
56
+ rspec (3.10.0)
57
+ rspec-core (~> 3.10.0)
58
+ rspec-expectations (~> 3.10.0)
59
+ rspec-mocks (~> 3.10.0)
60
+ rspec-core (3.10.1)
61
+ rspec-support (~> 3.10.0)
62
+ rspec-expectations (3.10.1)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.10.0)
65
+ rspec-mocks (3.10.2)
66
+ diff-lcs (>= 1.2.0, < 2.0)
67
+ rspec-support (~> 3.10.0)
68
+ rspec-support (3.10.3)
69
+
70
+ PLATFORMS
71
+ ruby
72
+
73
+ DEPENDENCIES
74
+ dalli
75
+ dry-auto_inject
76
+ dry-container
77
+ dry-monads
78
+ dry-schema
79
+ dry-struct
80
+ hydra-keycloak-client!
81
+ jwt
82
+ pry
83
+ rake (~> 12.0)
84
+ redis
85
+ rspec (~> 3.0)
86
+
87
+ BUNDLED WITH
88
+ 2.2.25
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Hydra::Keycloak::Client
2
+
3
+ Keycloak client for SSO implementation.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'hydra-keycloak-client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install hydra-keycloak-client
20
+
21
+ ## Usage
22
+
23
+ Create client instance for usage:
24
+
25
+ ```
26
+ keycloack_client = Hydra::Keycloak::ClientCreator.call(
27
+ config: {
28
+ auth_server_url: "http://0.0.0.0:8080/auth/",
29
+ realm: "hydra",
30
+ client_id: "hoper",
31
+ redirect_uri: "http://localhost:3000/authenticate_by_keycloak",
32
+ secret: "1fad7395-de0e-456a-a559-2896aa82f5e3",
33
+ logout_redirect: "http://localhost:3000",
34
+ memcached_host: "localhost",
35
+ memcached_port: "11211",
36
+ memcached_namespace: "hoper",
37
+ })
38
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hydra/keycloak/client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,28 @@
1
+ require './lib/hydra/keycloak/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.require_paths = ["lib"]
5
+ spec.name = "hydra-keycloak-client"
6
+ spec.version = Hydra::Keycloak::VERSION
7
+ spec.authors = ["Fedor Kosolapov"]
8
+ spec.email = ["f.kosolapov@latera.ru"]
9
+
10
+ spec.summary = "Keycloak client for SSO"
11
+ spec.description = "Keycloak client for SSO"
12
+ spec.homepage = "https://github.com/latera/hydra-keycloak-client"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "bin"
27
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+ require 'dry/monads'
3
+ require 'dry/auto_inject'
4
+ require 'dry/container'
5
+ require 'dry/schema'
6
+
7
+ require 'hydra/keycloak/container'
8
+ require 'hydra/keycloak/user_data'
9
+ require 'hydra/keycloak/code_verifier'
10
+
11
+ module Hydra
12
+ module Keycloak
13
+ class ConfigurationError < ::StandardError; end
14
+
15
+ class ClientCreator
16
+ extend ::Hydra::Keycloak::Mixin
17
+
18
+ def self.call(config:)
19
+ config_schema = Dry::Schema.JSON do
20
+ required(:auth_server_url).filled(:string)
21
+ required(:realm).filled(:string)
22
+ required(:client_id).filled(:string)
23
+ required(:redirect_uri).filled(:string)
24
+ required(:secret).filled(:string)
25
+ required(:logout_redirect).filled(:string)
26
+ required(:store_client).value(included_in?: ['redis', 'memcached'])
27
+
28
+ optional(:memcached).hash do
29
+ required(:memcached_host).filled(:string)
30
+ required(:memcached_port).filled(:string)
31
+ required(:memcached_namespace).filled(:string)
32
+ end
33
+
34
+ optional(:redis).hash do
35
+ required(:redis_host).filled(:string)
36
+ required(:redis_port).filled(:string)
37
+ end
38
+ end
39
+
40
+ validated_config = config_schema.call(config)
41
+
42
+ if validated_config.failure?
43
+ raise ConfigurationError, "Wrong configuration params: #{validated_config.errors(full: true).to_h}"
44
+ end
45
+
46
+ container.register :urls do
47
+ require 'hydra/keycloak/urls'
48
+
49
+ ::Hydra::Keycloak::Urls.new(validated_config)
50
+ end
51
+
52
+ container.register :queries do
53
+ require 'hydra/keycloak/queries/gateway'
54
+
55
+ ::Hydra::Keycloak::Queries::Gateway.new
56
+ end
57
+
58
+ if validated_config[:store_client] == :redis
59
+ container.register :redis do
60
+ require 'redis'
61
+
62
+ ::Redis.new(host: validated_config[:redis_host], port: validated_config[:redis_port])
63
+ end
64
+
65
+ container.register :store_client do
66
+ require 'hydra/keycloak/store/redis_client'
67
+
68
+ ::Hydra::Keycloak::Store::RedisClient.new
69
+ end
70
+ else validated_config[:store_client] == :memcached
71
+ container.register :dalli do
72
+ require 'dalli'
73
+
74
+ ::Dalli::Client.new(
75
+ "#{validated_config[:memcached_host]}:#{validated_config[:memcached_port]}",
76
+ namespace: validated_config[:memcached_namespace]
77
+ )
78
+ end
79
+
80
+ container.register :store_client do
81
+ require 'hydra/keycloak/store/memcached_client'
82
+
83
+ ::Hydra::Keycloak::Store::MemcachedClient.new
84
+ end
85
+ end
86
+
87
+ container.register :store do
88
+ require 'hydra/keycloak/store/gateway'
89
+
90
+ ::Hydra::Keycloak::Store::Gateway.new
91
+ end
92
+
93
+ container.register(:code_verifier, ::Hydra::Keycloak::CodeVerifier.new)
94
+
95
+ ::Hydra::Keycloak::Client.new
96
+ end
97
+ end
98
+
99
+ class Client
100
+ extend ::Hydra::Keycloak::Mixin
101
+ include ::Dry::Monads[:result, :do]
102
+ inject['urls', 'queries', 'store', 'code_verifier']
103
+
104
+ def auth_url
105
+ code_verifier.generate
106
+ urls.auth_url(code_verifier.code_challenge)
107
+ end
108
+
109
+ def authenticate!(auth_code)
110
+ unless auth_code
111
+ return Failure(status: 400, code: :auth_code_was_not_received)
112
+ end
113
+
114
+ queries.get_tokens(auth_code, code_verifier.value).fmap do |tokens|
115
+ access_token = tokens[:access_token]
116
+ id_token = tokens[:id_token]
117
+ refresh_token = tokens[:refresh_token]
118
+
119
+ session_state = access_token.session_state
120
+
121
+ save_token(session_state, 'access_token', access_token)
122
+ save_token(session_state, 'id_token', id_token)
123
+ save_token(session_state, 'refresh_token', refresh_token)
124
+
125
+ session_state
126
+ end
127
+ end
128
+
129
+ def authenticated?(session_state)
130
+ fetch_token(session_state, 'access_token').success?
131
+ end
132
+
133
+ def user_data(session_state)
134
+ unless authenticated?(session_state)
135
+ return Failure(status: 400, code: :not_authenticated)
136
+ end
137
+
138
+ fetch_token(session_state, 'access_token').fmap do |access_token|
139
+ UserData.new(
140
+ username: access_token[:login],
141
+ base_subject_id: access_token[:base_subject_id],
142
+ subj_group_id: access_token[:subj_group_id],
143
+ firm_id: access_token[:firm_id],
144
+ base_subject_first_name: access_token[:base_subject_first_name],
145
+ jti: access_token[:jti])
146
+ end
147
+ end
148
+
149
+ def access_token(session_state)
150
+ unless authenticated?(session_state)
151
+ return Failure(status: 400, code: :not_authenticated)
152
+ end
153
+
154
+ fetch_token(session_state, 'access_token')
155
+ end
156
+
157
+ def authorize!(session_state)
158
+ unless authenticated?(session_state)
159
+ return Failure(status: 400, code: :not_authenticated)
160
+ end
161
+
162
+ access_token = yield fetch_token(session_state, 'access_token')
163
+ if token_expired?(access_token)
164
+ refresh_tokens(session_state)
165
+
166
+ access_token = yield fetch_token(session_state, 'access_token')
167
+ end
168
+
169
+ queries.token_introspect(access_token.source)
170
+ end
171
+
172
+ def access_token_jti(session_state)
173
+ unless authenticated?(session_state)
174
+ return Failure(status: 400, code: :not_authenticated)
175
+ end
176
+
177
+ fetch_token(session_state, 'access_token').fmap do |access_token|
178
+ access_token.jti
179
+ end
180
+ end
181
+
182
+ def logout!(session_state)
183
+ fetch_token(session_state, 'id_token').bind do |id_token|
184
+ clear_tokens(session_state)
185
+
186
+ urls.end_session_url(id_token.source)
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def save_token(session_state, token_name, token)
193
+ store.set("#{session_state}_#{token_name}", token.source)
194
+ end
195
+
196
+ def fetch_token(session_state, token_name)
197
+ store.get("#{session_state}_#{token_name}").bind do |value|
198
+ if value
199
+ Success(::Hydra::Keycloak::Token.new(value))
200
+ else
201
+ Failure(status: 400, code: :token_not_found)
202
+ end
203
+ end
204
+ end
205
+
206
+ def clear_tokens(session_state)
207
+ store.delete("#{session_state}_access_token")
208
+ store.delete("#{session_state}_id_token")
209
+ store.delete("#{session_state}_refresh_token")
210
+ end
211
+
212
+ def token_expired?(token)
213
+ token.exp <= Time.now.to_i
214
+ end
215
+
216
+ def refresh_tokens(session_state)
217
+ refresh_token = yield fetch_token(session_state, 'refresh_token')
218
+ new_tokens = yield queries.refresh_tokens(refresh_token.source)
219
+
220
+ yield save_token(session_state, 'access_token', new_tokens[:access_token])
221
+ yield save_token(session_state, 'id_token', new_tokens[:id_token])
222
+ yield save_token(session_state, 'refresh_token', new_tokens[:refresh_token])
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "securerandom"
5
+
6
+ require 'hydra/keycloak/container'
7
+
8
+ module Hydra
9
+ module Keycloak
10
+ class CodeVerifier
11
+ attr_reader :value, :code_challenge
12
+
13
+ def generate
14
+ @value = _generate
15
+ @code_challenge = _generate_pkce(@value)
16
+ end
17
+
18
+ private
19
+
20
+ def _generate
21
+ # https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
22
+ charset = Array('A'..'Z') + Array('a'..'z') + Array(0..9)
23
+ charset.push("-").push(".").push("_").push("~")
24
+ Array.new(128) { charset.sample }.join
25
+ end
26
+
27
+ def _generate_pkce(code_verifier)
28
+ # https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
29
+ Digest::SHA256.base64digest(code_verifier).tr("+/", "-_").tr("=", "")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/auto_inject'
4
+ require 'dry/container'
5
+
6
+ module Hydra
7
+ module Keycloak
8
+ class Container
9
+ extend Dry::Container::Mixin
10
+
11
+ register(:http_client) do
12
+ require 'hydra/keycloak/queries/http_client'
13
+
14
+ ::Hydra::Keycloak::Queries::HttpClient.new
15
+ end
16
+ end
17
+
18
+ Import = Dry::AutoInject(Container)
19
+
20
+ class << self
21
+ def inject(target)
22
+ -> *values { target.send(:include, Import[*values]) }
23
+ end
24
+
25
+ def args_inject(target)
26
+ -> *values { target.send(:include, Import.args[*values]) }
27
+ end
28
+ end
29
+
30
+ module Mixin
31
+ def container
32
+ ::Hydra::Keycloak::Container
33
+ end
34
+
35
+ def inject
36
+ ::Hydra::Keycloak.inject(self)
37
+ end
38
+
39
+ def args_inject
40
+ ::Hydra::Keycloak.args_inject(self)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'dry/monads'
5
+ require 'hydra/keycloak/container'
6
+ require 'hydra/keycloak/token'
7
+
8
+ module Hydra
9
+ module Keycloak
10
+ module Queries
11
+ class Gateway
12
+ extend ::Hydra::Keycloak::Mixin
13
+ include ::Dry::Monads[:result]
14
+ inject['http_client', 'urls']
15
+
16
+ def get_tokens(auth_code, code_verifier)
17
+ result = make_request(urls.token_endpoint,
18
+ urls.token_request_body(auth_code, code_verifier))
19
+
20
+ result.fmap do |tokens|
21
+ {
22
+ access_token: ::Hydra::Keycloak::Token.new(tokens['access_token']),
23
+ id_token: ::Hydra::Keycloak::Token.new(tokens['id_token']),
24
+ refresh_token: ::Hydra::Keycloak::Token.new(tokens['refresh_token'])
25
+ }
26
+ end
27
+ end
28
+
29
+ def token_introspect(token)
30
+ make_request(urls.introspection_endpoint,
31
+ urls.introspection_request_body(token)).bind do |result|
32
+ if result['active']
33
+ Success(result)
34
+ else
35
+ Failure(status: 400, code: :token_not_active)
36
+ end
37
+ end
38
+ end
39
+
40
+ def refresh_tokens(refresh_token)
41
+ make_request(urls.token_endpoint,
42
+ urls.refresh_request_body(refresh_token)).bind do |result|
43
+ if result['error']
44
+ Failure(status: 400, code: :token_refreshing_error)
45
+ else
46
+ Success({
47
+ access_token: ::Hydra::Keycloak::Token.new(result['access_token']),
48
+ id_token: ::Hydra::Keycloak::Token.new(result['id_token']),
49
+ refresh_token: ::Hydra::Keycloak::Token.new(result['refresh_token'])
50
+ })
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def make_request(path, body)
58
+ http_client.do_post_request(path, body)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'dry/monads'
6
+
7
+ module Hydra
8
+ module Keycloak
9
+ module Queries
10
+ class HttpClient
11
+ include ::Dry::Monads[:result]
12
+
13
+ NetworkErrors = [Timeout::Error,
14
+ Errno::EINVAL,
15
+ Errno::ECONNRESET,
16
+ EOFError,
17
+ Errno::ECONNREFUSED,
18
+ Net::HTTPBadResponse,
19
+ Net::HTTPHeaderSyntaxError,
20
+ Net::ProtocolError]
21
+
22
+ def do_post_request(path, body)
23
+ response = Net::HTTP.post_form(URI(path), **body)
24
+
25
+ if response.code == '200'
26
+ json = JSON.parse(response.body)
27
+
28
+ Success(json)
29
+ else
30
+ Failure(status: response.code, code: :bad_keycloak_response)
31
+ end
32
+ rescue *NetworkErrors
33
+ Failure(status: 400, code: :keycloak_unavailable)
34
+ rescue JSON::ParserError
35
+ Failure(status: 400, code: :json_parser_error)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hydra/keycloak/container'
4
+
5
+ module Hydra
6
+ module Keycloak
7
+ module Store
8
+ class Gateway
9
+ extend ::Hydra::Keycloak::Mixin
10
+ inject['store_client']
11
+
12
+ def set(key, value)
13
+ store_client.set(key, value)
14
+ end
15
+
16
+ def get(key)
17
+ store_client.get(key)
18
+ end
19
+
20
+ def delete(key)
21
+ store_client.delete(key)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dalli'
4
+ require 'dry/monads'
5
+ require 'dry/auto_inject'
6
+
7
+ module Hydra
8
+ module Keycloak
9
+ module Store
10
+ class MemcachedClient
11
+ extend ::Hydra::Keycloak::Mixin
12
+ include ::Dry::Monads[:result]
13
+ inject['dalli']
14
+
15
+ def set(key, value)
16
+ dalli.set(key, value)
17
+
18
+ Success(:ok)
19
+ rescue Dalli::DalliError
20
+ Failure(status: 400, code: :memcached_unavailable)
21
+ end
22
+
23
+ def get(key)
24
+ Success(dalli.get(key))
25
+ rescue Dalli::DalliError
26
+ Failure(status: 400, code: :memcached_unavailable)
27
+ end
28
+
29
+ def delete(key)
30
+ dalli.delete(key)
31
+
32
+ Success(:ok)
33
+ rescue Dalli::DalliError
34
+ Failure(status: 400, code: :memcached_unavailable)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Hydra
6
+ module Keycloak
7
+ class Token
8
+ attr_reader :source,
9
+ :exp,
10
+ :iat,
11
+ :auth_time,
12
+ :iss,
13
+ :session_state,
14
+ :scope,
15
+ :jti
16
+
17
+ def initialize(source)
18
+ @source = source
19
+ @data = ::JWT.decode(source, nil, false).first.transform_keys(&:to_sym)
20
+ end
21
+
22
+ %i(exp iat auth_time iss session_state scope jti).each do |field|
23
+ define_method(field) do
24
+ @data.fetch(field)
25
+ end
26
+ end
27
+
28
+ def [](key)
29
+ @data.fetch(key)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hydra
4
+ module Keycloak
5
+ class Urls
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def auth_url(code_challenge)
11
+ "#{@config[:auth_server_url]}" \
12
+ "realms/#{@config[:realm]}/protocol/openid-connect/auth/?" \
13
+ 'response_type=code&' \
14
+ "client_id=#{@config[:client_id]}&" \
15
+ "redirect_uri=#{@config[:redirect_uri]}&" \
16
+ "nonce=#{@config[:secret]}&" \
17
+ 'scope=openid&' \
18
+ "code_challenge=#{code_challenge}&" \
19
+ "code_challenge_method=S256"
20
+ end
21
+
22
+ def token_endpoint
23
+ "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/token"
24
+ end
25
+
26
+ def token_request_body(auth_code, code_verifier)
27
+ {
28
+ grant_type: 'authorization_code',
29
+ code: auth_code,
30
+ redirect_uri: @config[:redirect_uri],
31
+ client_id: @config[:client_id],
32
+ client_secret: @config[:secret],
33
+ code_verifier: code_verifier
34
+ }
35
+ end
36
+
37
+ def introspection_endpoint
38
+ "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/token/introspect"
39
+ end
40
+
41
+ def introspection_request_body(token)
42
+ {
43
+ token: token,
44
+ token_type_hint: 'access_token',
45
+ client_id: @config[:client_id],
46
+ client_secret: @config[:secret]
47
+ }
48
+ end
49
+
50
+ def end_session_url(id_token)
51
+ "#{@config[:auth_server_url]}realms/#{@config[:realm]}/protocol/openid-connect/logout" \
52
+ "?id_token_hint=#{id_token}" \
53
+ "&post_logout_redirect_uri=#{@config[:logout_redirect]}"
54
+ end
55
+
56
+ def refresh_request_body(refresh_token)
57
+ {
58
+ client_id: @config[:client_id],
59
+ client_secret: @config[:secret],
60
+ grant_type: 'refresh_token',
61
+ refresh_token: refresh_token,
62
+ scope: 'openid'
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/struct'
4
+
5
+ module Hydra
6
+ module Keycloak
7
+ module Types
8
+ include Dry.Types()
9
+ end
10
+
11
+ class UserData < Dry::Struct
12
+ attribute :username, Types::String
13
+ attribute :base_subject_id, Types::Coercible::Integer
14
+ attribute :subj_group_id, Types::Coercible::Integer
15
+ attribute :firm_id, Types::Coercible::Integer
16
+ attribute :base_subject_first_name, Types::String
17
+ attribute :jti, Types::String
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hydra
4
+ module Keycloak
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hydra-keycloak-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Fedor Kosolapov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-02-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Keycloak client for SSO
14
+ email:
15
+ - f.kosolapov@latera.ru
16
+ executables:
17
+ - console
18
+ - setup
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - README.md
25
+ - Rakefile
26
+ - bin/console
27
+ - bin/setup
28
+ - hydra-keycloak-client.gemspec
29
+ - lib/hydra/keycloak/client.rb
30
+ - lib/hydra/keycloak/code_verifier.rb
31
+ - lib/hydra/keycloak/container.rb
32
+ - lib/hydra/keycloak/queries/gateway.rb
33
+ - lib/hydra/keycloak/queries/http_client.rb
34
+ - lib/hydra/keycloak/store/gateway.rb
35
+ - lib/hydra/keycloak/store/memcached_client.rb
36
+ - lib/hydra/keycloak/token.rb
37
+ - lib/hydra/keycloak/urls.rb
38
+ - lib/hydra/keycloak/user_data.rb
39
+ - lib/hydra/keycloak/version.rb
40
+ homepage: https://github.com/latera/hydra-keycloak-client
41
+ licenses: []
42
+ metadata:
43
+ allowed_push_host: https://rubygems.org
44
+ homepage_uri: https://github.com/latera/hydra-keycloak-client
45
+ source_code_uri: https://github.com/latera/hydra-keycloak-client
46
+ changelog_uri: https://github.com/latera/hydra-keycloak-client
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.3.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.1.6
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Keycloak client for SSO
66
+ test_files: []