hydra-keycloak-client 0.1.1

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 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: []