keycloak_ruby 0.1.2 → 0.1.3
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 +4 -4
- data/.reek.yml +9 -0
- data/.rspec_status +70 -66
- data/Rakefile +4 -2
- data/lib/keycloak_ruby/authentication.rb +6 -0
- data/lib/keycloak_ruby/client.rb +21 -21
- data/lib/keycloak_ruby/config.rb +21 -3
- data/lib/keycloak_ruby/request_performer.rb +13 -18
- data/lib/keycloak_ruby/response_validator.rb +9 -6
- data/lib/keycloak_ruby/testing/keycloak_helpers.rb +10 -10
- data/lib/keycloak_ruby/token_service.rb +6 -4
- data/lib/keycloak_ruby/user.rb +3 -2
- data/lib/keycloak_ruby/version.rb +2 -2
- data/lib/keycloak_ruby.rb +29 -7
- data/sig/keycloak_ruby.rbs +17 -634
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b5e24bb870789d3bfef9291dbb90ad2ad87a33e8786320fd2dbe0c759da7f8c
|
4
|
+
data.tar.gz: ceebf27600e81da2c16ffb1b82b6cba7e1a0d0176489c138fbf80ecab0aa3069
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '054193e51f510f205ed667fd34a292ff89761fab3af75b77304d7aafa27680b9f08d0d0ba6d068471d417459812de443587f0689cd0efec9b98dfdecb5ddf109'
|
7
|
+
data.tar.gz: d9d7060295a053e533fe105fbb157bc1e0ff267bc2b78d9a17654149816dc76a3cb9fb37314233276c8f7b8f48abedc89ed3a0de36ec745d6a34e95f04c77834
|
data/.reek.yml
ADDED
data/.rspec_status
CHANGED
@@ -1,68 +1,72 @@
|
|
1
1
|
example_id | status | run_time |
|
2
2
|
-------------------------------------------------------- | ------ | --------------- |
|
3
|
-
./spec/keycloak_ruby/client_spec.rb[1:1:1:1] | passed | 0.
|
4
|
-
./spec/keycloak_ruby/client_spec.rb[1:1:2:1] | passed | 0.
|
5
|
-
./spec/keycloak_ruby/client_spec.rb[1:2:1:1] | passed | 0.
|
6
|
-
./spec/keycloak_ruby/client_spec.rb[1:2:2:1] | passed | 0.
|
7
|
-
./spec/keycloak_ruby/client_spec.rb[1:3:1] | passed | 0.
|
8
|
-
./spec/keycloak_ruby/client_spec.rb[1:3:2] | passed | 0.
|
9
|
-
./spec/keycloak_ruby/client_spec.rb[1:4:1:1] | passed | 0.
|
10
|
-
./spec/keycloak_ruby/client_spec.rb[1:4:2:1] | passed | 0.
|
11
|
-
./spec/keycloak_ruby/client_spec.rb[1:5:1] | passed | 0.
|
12
|
-
./spec/keycloak_ruby/client_spec.rb[1:6:1:1] | passed | 0.
|
13
|
-
./spec/keycloak_ruby/client_spec.rb[1:6:2:1] | passed | 0.
|
14
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:1:1] | passed | 0.
|
15
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:1:2] | passed | 0.
|
16
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:1:3] | passed | 0.
|
17
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:1:4] | passed | 0.
|
18
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:1:5] | passed | 0.
|
19
|
-
./spec/keycloak_ruby/config_spec.rb[1:1:2:1] | passed | 0.
|
20
|
-
./spec/keycloak_ruby/config_spec.rb[1:2:1] | passed | 0.
|
21
|
-
./spec/keycloak_ruby/config_spec.rb[1:2:2] | passed | 0.
|
22
|
-
./spec/keycloak_ruby/config_spec.rb[1:2:3] | passed | 0.
|
23
|
-
./spec/keycloak_ruby/config_spec.rb[1:2:4] | passed | 0.
|
24
|
-
./spec/keycloak_ruby/config_spec.rb[1:3:1] | passed | 0.
|
25
|
-
./spec/keycloak_ruby/config_spec.rb[1:3:2] | passed | 0.
|
26
|
-
./spec/keycloak_ruby/config_spec.rb[1:3:3] | passed | 0.
|
27
|
-
./spec/keycloak_ruby/config_spec.rb[1:3:4] | passed | 0.
|
28
|
-
./spec/keycloak_ruby/config_spec.rb[1:4:1:1] | passed | 0.
|
29
|
-
./spec/keycloak_ruby/config_spec.rb[1:4:2:1] | passed | 0.
|
30
|
-
./spec/keycloak_ruby/config_spec.rb[1:4:2:2] | passed | 0.
|
31
|
-
./spec/keycloak_ruby/request_performer_spec.rb[1:1:1:1] | passed | 0.
|
32
|
-
./spec/keycloak_ruby/request_performer_spec.rb[1:1:1:2] | passed | 0.
|
33
|
-
./spec/keycloak_ruby/request_performer_spec.rb[1:1:2:1] | passed | 0.
|
34
|
-
./spec/keycloak_ruby/request_performer_spec.rb[1:1:3:1] | passed | 0.
|
35
|
-
./spec/keycloak_ruby/request_performer_spec.rb[1:1:4:1] | passed | 0.
|
36
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:1:1] | passed | 0.
|
37
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:2:1] | passed | 0.
|
38
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:3:1] | passed | 0.
|
39
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:4:1] | passed | 0.
|
40
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:5:1] | passed | 0.
|
41
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:1:6:1] | passed | 0.
|
42
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:1:1] | passed | 0.
|
43
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:2:1] | passed | 0.
|
44
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:3:1] | passed | 0.
|
45
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:4:1] | passed | 0.
|
46
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:5:1] | passed | 0.
|
47
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:6:1] | passed | 0.
|
48
|
-
./spec/keycloak_ruby/response_validator_spec.rb[1:2:7:1] | passed | 0.
|
49
|
-
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:1:1] | passed | 0.
|
50
|
-
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:1:2] | passed | 0.
|
51
|
-
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:2:1] | passed | 0.
|
52
|
-
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:3:1] | passed | 0.
|
53
|
-
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:4:1] | passed | 0.
|
54
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:1:1:1] | passed | 0.
|
55
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:1:2:1] | passed | 0.
|
56
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:1:2:2] | passed | 0.
|
57
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:2:1] | passed | 0.
|
58
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:2:2] | passed | 0.
|
59
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:2:3] | passed | 0.
|
60
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:3:1] | passed | 0.
|
61
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:3:2] | passed | 0.
|
62
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:3:3] | passed | 0.
|
63
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:4:1:1] | passed | 0.
|
64
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:4:2:1] | passed | 0.
|
65
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:4:3:1] | passed | 0.
|
66
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:4:3:2] | passed | 0.
|
67
|
-
./spec/keycloak_ruby/token_service_spec.rb[1:5:1] | passed | 0.
|
68
|
-
./spec/keycloak_ruby_spec.rb[1:1] | passed | 0.
|
3
|
+
./spec/keycloak_ruby/client_spec.rb[1:1:1:1] | passed | 0.0256 seconds |
|
4
|
+
./spec/keycloak_ruby/client_spec.rb[1:1:2:1] | passed | 0.00494 seconds |
|
5
|
+
./spec/keycloak_ruby/client_spec.rb[1:2:1:1] | passed | 0.01574 seconds |
|
6
|
+
./spec/keycloak_ruby/client_spec.rb[1:2:2:1] | passed | 0.00294 seconds |
|
7
|
+
./spec/keycloak_ruby/client_spec.rb[1:3:1] | passed | 0.00867 seconds |
|
8
|
+
./spec/keycloak_ruby/client_spec.rb[1:3:2] | passed | 0.00564 seconds |
|
9
|
+
./spec/keycloak_ruby/client_spec.rb[1:4:1:1] | passed | 0.00271 seconds |
|
10
|
+
./spec/keycloak_ruby/client_spec.rb[1:4:2:1] | passed | 0.00267 seconds |
|
11
|
+
./spec/keycloak_ruby/client_spec.rb[1:5:1] | passed | 0.00413 seconds |
|
12
|
+
./spec/keycloak_ruby/client_spec.rb[1:6:1:1] | passed | 0.00466 seconds |
|
13
|
+
./spec/keycloak_ruby/client_spec.rb[1:6:2:1] | passed | 0.00228 seconds |
|
14
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:1:1] | passed | 0.00122 seconds |
|
15
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:1:2] | passed | 0.00104 seconds |
|
16
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:1:3] | passed | 0.0009 seconds |
|
17
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:1:4] | passed | 0.00113 seconds |
|
18
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:1:5] | passed | 0.00111 seconds |
|
19
|
+
./spec/keycloak_ruby/config_spec.rb[1:1:2:1] | passed | 0.00025 seconds |
|
20
|
+
./spec/keycloak_ruby/config_spec.rb[1:2:1] | passed | 0.00117 seconds |
|
21
|
+
./spec/keycloak_ruby/config_spec.rb[1:2:2] | passed | 0.00122 seconds |
|
22
|
+
./spec/keycloak_ruby/config_spec.rb[1:2:3] | passed | 0.00097 seconds |
|
23
|
+
./spec/keycloak_ruby/config_spec.rb[1:2:4] | passed | 0.00092 seconds |
|
24
|
+
./spec/keycloak_ruby/config_spec.rb[1:3:1] | passed | 0.00076 seconds |
|
25
|
+
./spec/keycloak_ruby/config_spec.rb[1:3:2] | passed | 0.00078 seconds |
|
26
|
+
./spec/keycloak_ruby/config_spec.rb[1:3:3] | passed | 0.00059 seconds |
|
27
|
+
./spec/keycloak_ruby/config_spec.rb[1:3:4] | passed | 0.00073 seconds |
|
28
|
+
./spec/keycloak_ruby/config_spec.rb[1:4:1:1] | passed | 0.01004 seconds |
|
29
|
+
./spec/keycloak_ruby/config_spec.rb[1:4:2:1] | passed | 0.0012 seconds |
|
30
|
+
./spec/keycloak_ruby/config_spec.rb[1:4:2:2] | passed | 0.00094 seconds |
|
31
|
+
./spec/keycloak_ruby/request_performer_spec.rb[1:1:1:1] | passed | 0.00226 seconds |
|
32
|
+
./spec/keycloak_ruby/request_performer_spec.rb[1:1:1:2] | passed | 0.00168 seconds |
|
33
|
+
./spec/keycloak_ruby/request_performer_spec.rb[1:1:2:1] | passed | 0.00181 seconds |
|
34
|
+
./spec/keycloak_ruby/request_performer_spec.rb[1:1:3:1] | passed | 0.00152 seconds |
|
35
|
+
./spec/keycloak_ruby/request_performer_spec.rb[1:1:4:1] | passed | 0.00155 seconds |
|
36
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:1:1] | passed | 0.00203 seconds |
|
37
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:2:1] | passed | 0.00056 seconds |
|
38
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:3:1] | passed | 0.00053 seconds |
|
39
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:4:1] | passed | 0.00034 seconds |
|
40
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:5:1] | passed | 0.00028 seconds |
|
41
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:1:6:1] | passed | 0.00033 seconds |
|
42
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:1:1] | passed | 0.00032 seconds |
|
43
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:2:1] | passed | 0.00042 seconds |
|
44
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:3:1] | passed | 0.00034 seconds |
|
45
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:4:1] | passed | 0.00044 seconds |
|
46
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:5:1] | passed | 0.00042 seconds |
|
47
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:6:1] | passed | 0.00035 seconds |
|
48
|
+
./spec/keycloak_ruby/response_validator_spec.rb[1:2:7:1] | passed | 0.00032 seconds |
|
49
|
+
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:1:1] | passed | 0.00247 seconds |
|
50
|
+
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:1:2] | passed | 0.00199 seconds |
|
51
|
+
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:2:1] | passed | 0.00237 seconds |
|
52
|
+
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:3:1] | passed | 0.0017 seconds |
|
53
|
+
./spec/keycloak_ruby/token_refresher_spec.rb[1:1:4:1] | passed | 0.0022 seconds |
|
54
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:1:1:1] | passed | 0.01982 seconds |
|
55
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:1:2:1] | passed | 0.00324 seconds |
|
56
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:1:2:2] | passed | 0.0209 seconds |
|
57
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:2:1] | passed | 0.00223 seconds |
|
58
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:2:2] | passed | 0.00208 seconds |
|
59
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:2:3] | passed | 0.00168 seconds |
|
60
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:3:1] | passed | 0.00144 seconds |
|
61
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:3:2] | passed | 0.00124 seconds |
|
62
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:3:3] | passed | 0.0013 seconds |
|
63
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:4:1:1] | passed | 0.00157 seconds |
|
64
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:4:2:1] | passed | 0.00238 seconds |
|
65
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:4:3:1] | passed | 0.00333 seconds |
|
66
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:4:3:2] | passed | 0.0019 seconds |
|
67
|
+
./spec/keycloak_ruby/token_service_spec.rb[1:5:1] | passed | 0.00224 seconds |
|
68
|
+
./spec/keycloak_ruby_spec.rb[1:1] | passed | 0.00021 seconds |
|
69
|
+
./spec/keycloak_ruby_spec.rb[1:2] | passed | 0.01876 seconds |
|
70
|
+
./spec/keycloak_ruby_spec.rb[1:3:1] | passed | 0.00099 seconds |
|
71
|
+
./spec/keycloak_ruby_spec.rb[1:3:2] | passed | 0.00095 seconds |
|
72
|
+
./spec/keycloak_ruby_spec.rb[1:3:3] | passed | 0.00024 seconds |
|
data/Rakefile
CHANGED
@@ -9,6 +9,8 @@ module KeycloakRuby
|
|
9
9
|
|
10
10
|
included do
|
11
11
|
before_action :authenticate_user!
|
12
|
+
|
13
|
+
helper_method :current_user, :user_signed_in?
|
12
14
|
end
|
13
15
|
|
14
16
|
def keycloak_jwt_service
|
@@ -22,5 +24,9 @@ module KeycloakRuby
|
|
22
24
|
def current_user
|
23
25
|
@current_user ||= keycloak_jwt_service.find_user
|
24
26
|
end
|
27
|
+
|
28
|
+
def user_signed_in?
|
29
|
+
current_user.present?
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
data/lib/keycloak_ruby/client.rb
CHANGED
@@ -44,7 +44,7 @@ module KeycloakRuby
|
|
44
44
|
# @raise [KeycloakRuby::Errors::UserCreationError] If user creation fails.
|
45
45
|
# @return [Hash] The newly created user's data.
|
46
46
|
def create_user(user_attrs = {})
|
47
|
-
user_data = build_user_data(user_attrs)
|
47
|
+
user_data = self.class.build_user_data(user_attrs)
|
48
48
|
|
49
49
|
response = http_request(
|
50
50
|
http_method: :post,
|
@@ -112,19 +112,7 @@ module KeycloakRuby
|
|
112
112
|
update_redirect_uris_for(client_record["id"], redirect_uris)
|
113
113
|
end
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
def build_auth_body(username, password)
|
118
|
-
{
|
119
|
-
client_id: @config.oauth_client_id,
|
120
|
-
client_secret: @config.oauth_client_secret,
|
121
|
-
username: username,
|
122
|
-
password: password,
|
123
|
-
grant_type: "password"
|
124
|
-
}
|
125
|
-
end
|
126
|
-
|
127
|
-
def build_user_data(attrs)
|
115
|
+
def self.build_user_data(attrs)
|
128
116
|
{ username: attrs.fetch(:username), email: attrs.fetch(:email), enabled: true,
|
129
117
|
credentials: [
|
130
118
|
{
|
@@ -135,13 +123,7 @@ module KeycloakRuby
|
|
135
123
|
] }
|
136
124
|
end
|
137
125
|
|
138
|
-
|
139
|
-
def http_request(options = {})
|
140
|
-
params = build_request_params(options)
|
141
|
-
@request_performer.call(params)
|
142
|
-
end
|
143
|
-
|
144
|
-
def build_request_params(opts)
|
126
|
+
def self.build_request_params(opts)
|
145
127
|
RequestParams.new(
|
146
128
|
http_method: opts.fetch(:http_method),
|
147
129
|
url: opts.fetch(:url),
|
@@ -153,6 +135,24 @@ module KeycloakRuby
|
|
153
135
|
)
|
154
136
|
end
|
155
137
|
|
138
|
+
private
|
139
|
+
|
140
|
+
def build_auth_body(username, password)
|
141
|
+
{
|
142
|
+
client_id: @config.oauth_client_id,
|
143
|
+
client_secret: @config.oauth_client_secret,
|
144
|
+
username: username,
|
145
|
+
password: password,
|
146
|
+
grant_type: "password"
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
# Builds RequestParams and passes them to the RequestPerformer.
|
151
|
+
def http_request(options = {})
|
152
|
+
params = self.class.build_request_params(options)
|
153
|
+
@request_performer.call(params)
|
154
|
+
end
|
155
|
+
|
156
156
|
# Retrieves client details by its "clientId".
|
157
157
|
#
|
158
158
|
# @param client_id [String] The "clientId" in Keycloak.
|
data/lib/keycloak_ruby/config.rb
CHANGED
@@ -39,6 +39,10 @@ module KeycloakRuby
|
|
39
39
|
oauth_client_id
|
40
40
|
oauth_client_secret
|
41
41
|
].freeze
|
42
|
+
|
43
|
+
# Default environment
|
44
|
+
DEFAULT_ENV = "development"
|
45
|
+
|
42
46
|
# :reek:Attribute
|
43
47
|
attr_accessor :keycloak_url,
|
44
48
|
:app_host,
|
@@ -91,7 +95,6 @@ module KeycloakRuby
|
|
91
95
|
private
|
92
96
|
|
93
97
|
# Loads configuration from YAML file if it exists
|
94
|
-
# :reek:ManualDispatch
|
95
98
|
def load_config
|
96
99
|
return unless File.exist?(@config_path)
|
97
100
|
|
@@ -102,16 +105,31 @@ module KeycloakRuby
|
|
102
105
|
|
103
106
|
def load_yaml_file
|
104
107
|
YAML.safe_load(ERB.new(File.read(@config_path)).result, aliases: true)
|
108
|
+
rescue Errno::ENOENT, Psych::SyntaxError => e
|
109
|
+
raise Errors::ConfigurationError, "Failed to load YAML from #{@config_path}: #{e.message}"
|
105
110
|
end
|
106
111
|
|
112
|
+
# Determines current environment
|
113
|
+
#
|
114
|
+
# @return [String] Current environment name
|
115
|
+
# @api private
|
107
116
|
def current_env
|
108
|
-
defined?(Rails)
|
117
|
+
@current_env ||= (defined?(Rails) && Rails.env) || ENV["APP_ENV"] || DEFAULT_ENV
|
109
118
|
end
|
110
119
|
|
120
|
+
# Applies configuration from hash
|
121
|
+
#
|
122
|
+
# @param config_hash [Hash] Configuration key-value pairs
|
123
|
+
# @api private
|
111
124
|
def apply_config(config_hash)
|
112
125
|
config_hash.each do |key, value|
|
113
126
|
setter = :"#{key}="
|
114
|
-
|
127
|
+
begin
|
128
|
+
public_send(setter, value)
|
129
|
+
rescue NoMethodError
|
130
|
+
# Silently ignore unknown configuration keys
|
131
|
+
next
|
132
|
+
end
|
115
133
|
end
|
116
134
|
end
|
117
135
|
end
|
@@ -21,27 +21,21 @@ module KeycloakRuby
|
|
21
21
|
# @raise [request_params.error_class] If the response code is not in success_codes
|
22
22
|
# or HTTParty raises an error.
|
23
23
|
def call(request_params)
|
24
|
-
|
25
|
-
|
26
|
-
url = request_params.url
|
27
|
-
headers = request_params.headers
|
28
|
-
body = request_params.body
|
29
|
-
|
30
|
-
response = HTTParty.send(http_method, url, headers: headers, body: body)
|
24
|
+
request_options = request_params.to_h.slice(:headers, :body)
|
25
|
+
response = HTTParty.send(request_params.http_method, request_params.url, **request_options)
|
31
26
|
verify_response!(response, request_params)
|
32
27
|
response
|
33
28
|
rescue HTTParty::Error => e
|
34
|
-
|
35
|
-
|
29
|
+
message = e.message
|
30
|
+
KeycloakRuby.logger.error("#{request_params.error_message} (HTTParty error): #{message}")
|
31
|
+
raise request_params.error_class, message
|
36
32
|
end
|
37
33
|
|
38
34
|
private
|
39
35
|
|
40
36
|
# Safe validation: returns true/false
|
41
|
-
|
42
|
-
|
43
|
-
success_codes = request_params.success_codes
|
44
|
-
|
37
|
+
# :reek:UtilityFunction
|
38
|
+
def verify_response(code, success_codes)
|
45
39
|
case success_codes
|
46
40
|
when Range
|
47
41
|
success_codes.cover?(code)
|
@@ -54,12 +48,13 @@ module KeycloakRuby
|
|
54
48
|
|
55
49
|
# Bang version that raises an error on invalid response
|
56
50
|
def verify_response!(response, request_params)
|
57
|
-
|
51
|
+
code = response.code
|
52
|
+
return if verify_response(code, request_params.success_codes)
|
58
53
|
|
59
|
-
|
60
|
-
|
61
|
-
KeycloakRuby.logger.error("#{
|
62
|
-
raise request_params.error_class, "#{
|
54
|
+
message = request_params.error_message
|
55
|
+
body = response.body
|
56
|
+
KeycloakRuby.logger.error("#{message}: #{code} => #{body}")
|
57
|
+
raise request_params.error_class, "#{message}: #{code} => #{body}"
|
63
58
|
end
|
64
59
|
end
|
65
60
|
end
|
@@ -85,15 +85,16 @@ module KeycloakRuby
|
|
85
85
|
# Raises appropriate validation error based on failure reason
|
86
86
|
# @raise [KeycloakRuby::Errors::TokenRefreshFailed]
|
87
87
|
def raise_validation_error # rubocop:disable Metrics/MethodLength
|
88
|
+
error_description = @data["error_description"]
|
88
89
|
if !valid_http_status?
|
89
90
|
raise Errors::TokenRefreshFailed,
|
90
91
|
"Keycloak API request failed with status #{@response.code}: #{extract_error_message}"
|
91
92
|
elsif invalid_grant?
|
92
93
|
raise Errors::TokenRefreshFailed,
|
93
|
-
"Invalid grant: #{
|
94
|
+
"Invalid grant: #{error_description || "Refresh token invalid or expired"}"
|
94
95
|
elsif error_present?
|
95
96
|
raise Errors::TokenRefreshFailed,
|
96
|
-
"Keycloak error: #{@data["error"]} - #{
|
97
|
+
"Keycloak error: #{@data["error"]} - #{error_description}"
|
97
98
|
else
|
98
99
|
raise Errors::TokenRefreshFailed,
|
99
100
|
"Invalid response: access token missing from response"
|
@@ -103,10 +104,12 @@ module KeycloakRuby
|
|
103
104
|
# Extracts error message from response
|
104
105
|
# @return [String]
|
105
106
|
def extract_error_message
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
107
|
+
error_description = @data["error_description"]
|
108
|
+
response_body = @response.body
|
109
|
+
if error_description
|
110
|
+
error_description
|
111
|
+
elsif response_body.length < 100 # Prevent huge error messages
|
112
|
+
response_body
|
110
113
|
else
|
111
114
|
"See response body for details"
|
112
115
|
end
|
@@ -56,32 +56,31 @@ module KeycloakRuby
|
|
56
56
|
private
|
57
57
|
|
58
58
|
def mock_keycloak_login(user, use_capybara: true)
|
59
|
-
|
59
|
+
config = OmniAuth.config
|
60
|
+
config.test_mode = true
|
60
61
|
token_data = generate_fake_tokens(user)
|
61
|
-
|
62
|
+
config.mock_auth[:keycloak] = token_data
|
62
63
|
|
63
|
-
|
64
|
-
capybara_login
|
65
|
-
else
|
66
|
-
store_session(token_data.credentials)
|
67
|
-
end
|
64
|
+
use_capybara ? capybara_login : store_session(token_data.credentials)
|
68
65
|
end
|
69
66
|
|
70
67
|
def capybara_login
|
71
68
|
visit "/login"
|
72
|
-
|
69
|
+
translated_login_link = I18n.t("user.login")
|
70
|
+
click_on translated_login_link if page.has_button? translated_login_link
|
73
71
|
end
|
74
72
|
|
75
73
|
def generate_fake_tokens(user)
|
76
74
|
email = user.email
|
77
|
-
|
75
|
+
expired_time = 2.hours.from_now.to_i
|
76
|
+
token_payload = { "email" => email, "exp" => expired_time }
|
78
77
|
|
79
78
|
OmniAuth::AuthHash.new(provider: "keycloak", uid: "uid-#{email}", info: { email: email },
|
80
79
|
credentials: OmniAuth::AuthHash.new(
|
81
80
|
token: JWT.encode(token_payload, nil, "none"),
|
82
81
|
refresh_token: "fake-refresh-#{email}",
|
83
82
|
id_token: "fake-id-#{email}",
|
84
|
-
expires_at:
|
83
|
+
expires_at: expired_time
|
85
84
|
))
|
86
85
|
end
|
87
86
|
|
@@ -118,6 +117,7 @@ if defined?(RSpec)
|
|
118
117
|
end
|
119
118
|
elsif defined?(Minitest)
|
120
119
|
module Minitest
|
120
|
+
# Include helpers in minitest module
|
121
121
|
class Test
|
122
122
|
include KeycloakRuby::Testing::KeycloakHelpers
|
123
123
|
end
|
@@ -20,8 +20,12 @@ module KeycloakRuby
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# Store token
|
23
|
+
# :reek:DuplicateMethodCall
|
24
|
+
# :reek:FeatureEnvy
|
23
25
|
def store_tokens(data)
|
24
|
-
|
26
|
+
# It's necessary, because omniauth return request.env["omniauth.auth"] as 'token', not 'access_token'
|
27
|
+
|
28
|
+
@session[:access_token] = data["token"] || data["access_token"]
|
25
29
|
@session[:refresh_token] = data["refresh_token"] if data["refresh_token"]
|
26
30
|
@session[:id_token] = data["id_token"] if data["id_token"]
|
27
31
|
end
|
@@ -33,9 +37,6 @@ module KeycloakRuby
|
|
33
37
|
private
|
34
38
|
|
35
39
|
# It's necessary, because omniauth return request.env["omniauth.auth"] as 'token', not 'access_token'
|
36
|
-
def extract_access_token(data)
|
37
|
-
data["token"] || data["access_token"]
|
38
|
-
end
|
39
40
|
|
40
41
|
# Gets current token or attempts refresh if expired
|
41
42
|
# @return [Hash, nil] Decoded token claims
|
@@ -52,6 +53,7 @@ module KeycloakRuby
|
|
52
53
|
|
53
54
|
# Decodes JWT token
|
54
55
|
# @raise [Errors::TokenExpired, Errors::TokenInvalid]
|
56
|
+
# :reek:DuplicateMethodCall
|
55
57
|
def decode_token(token)
|
56
58
|
options = jwt_decode_options
|
57
59
|
|
data/lib/keycloak_ruby/user.rb
CHANGED
@@ -57,8 +57,9 @@ module KeycloakRuby
|
|
57
57
|
#
|
58
58
|
# @raise [KeycloakRuby::Errors::Error] if any request to Keycloak fails
|
59
59
|
def find_or_create(user_attrs = {})
|
60
|
-
|
61
|
-
|
60
|
+
user_email = user_attrs[:email]
|
61
|
+
users = find(user_email)
|
62
|
+
user = users.detect { |user| user["email"].casecmp?(user_email) }
|
62
63
|
|
63
64
|
if user
|
64
65
|
{ user_data: user, created: false }
|
@@ -5,8 +5,8 @@ module KeycloakRuby
|
|
5
5
|
# The current version of the KeycloakRuby gem as a string.
|
6
6
|
# Follows Semantic Versioning 2.0 (https://semver.org)
|
7
7
|
# @example
|
8
|
-
# KeycloakRuby::VERSION # => "0.1.
|
9
|
-
VERSION = "0.1.
|
8
|
+
# KeycloakRuby::VERSION # => "0.1.3"
|
9
|
+
VERSION = "0.1.3"
|
10
10
|
|
11
11
|
# Provides version information and comparison methods for the KeycloakRuby gem.
|
12
12
|
# This module follows Semantic Versioning 2.0 guidelines.
|
data/lib/keycloak_ruby.rb
CHANGED
@@ -6,7 +6,8 @@ require "omniauth_openid_connect"
|
|
6
6
|
require "httparty"
|
7
7
|
require "jwt"
|
8
8
|
require "zeitwerk"
|
9
|
-
|
9
|
+
require "omniauth/rails_csrf_protection/version"
|
10
|
+
require "omniauth/rails_csrf_protection/railtie" if defined?(Rails)
|
10
11
|
loader = Zeitwerk::Loader.for_gem
|
11
12
|
loader.ignore(
|
12
13
|
"#{__dir__}/generators",
|
@@ -24,15 +25,11 @@ module KeycloakRuby
|
|
24
25
|
# Defaults to Rails.logger if available, or a standard Logger.
|
25
26
|
#
|
26
27
|
# @return [Logger]
|
28
|
+
# :reek:Attribute
|
27
29
|
attr_writer :logger
|
28
30
|
|
29
31
|
def logger
|
30
|
-
@logger ||=
|
31
|
-
Rails.logger
|
32
|
-
else
|
33
|
-
require "logger"
|
34
|
-
Logger.new($stdout).tap { |log| log.level = Logger::INFO }
|
35
|
-
end
|
32
|
+
@logger ||= resolve_logger
|
36
33
|
end
|
37
34
|
|
38
35
|
# Returns the singleton configuration object. The configuration is
|
@@ -52,6 +49,31 @@ module KeycloakRuby
|
|
52
49
|
yield config
|
53
50
|
config.validate!
|
54
51
|
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def resolve_logger
|
56
|
+
if rails_defined? && rails_logger
|
57
|
+
rails_logger
|
58
|
+
else
|
59
|
+
default_logger
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def rails_defined?
|
64
|
+
defined?(Rails)
|
65
|
+
end
|
66
|
+
|
67
|
+
def rails_logger
|
68
|
+
Rails.logger if rails_defined?
|
69
|
+
rescue NoMethodError
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def default_logger
|
74
|
+
require "logger"
|
75
|
+
Logger.new($stdout).tap { |log| log.level = Logger::INFO }
|
76
|
+
end
|
55
77
|
end
|
56
78
|
# Load test helpers only in test environment
|
57
79
|
if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test" || defined?(RSpec) || defined?(Minitest)
|
data/sig/keycloak_ruby.rbs
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
# Module for interacting with Keycloak
|
2
2
|
module KeycloakRuby
|
3
|
+
self.@logger: untyped
|
4
|
+
|
3
5
|
self.@config: untyped
|
4
6
|
|
7
|
+
# Logger used throughout the gem
|
8
|
+
#
|
9
|
+
# Defaults to Rails.logger if available, or a standard Logger.
|
10
|
+
#
|
11
|
+
# @return [Logger]
|
12
|
+
# :reek:Attribute
|
13
|
+
attr_writer self.logger: untyped
|
14
|
+
|
15
|
+
def self.logger: () -> untyped
|
16
|
+
|
5
17
|
# Returns the singleton configuration object. The configuration is
|
6
18
|
# initialized on first access and validated immediately.
|
7
19
|
#
|
@@ -15,642 +27,13 @@ module KeycloakRuby
|
|
15
27
|
# @raise [ConfigurationError] if configuration is invalid
|
16
28
|
def self.configure: () { (untyped) -> untyped } -> untyped
|
17
29
|
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
# lib/keycloak_ruby/user.rb
|
22
|
-
module KeycloakRuby
|
23
|
-
# User-related operations for interacting with Keycloak.
|
24
|
-
# This class provides a simple interface for creating, deleting, and finding users in Keycloak.
|
25
|
-
class User
|
26
|
-
self.@client: untyped
|
27
|
-
|
28
|
-
# Creates a new user in Keycloak.
|
29
|
-
#
|
30
|
-
# @param user_attrs [Hash] A hash of user attributes (e.g., :username, :email, :password, :temporary).
|
31
|
-
# @return [Hash] The created user's data.
|
32
|
-
# @raise [KeycloakRuby::Error] If the user creation fails.
|
33
|
-
def self.create: (?::Hash[untyped, untyped] user_attrs) -> untyped
|
34
|
-
|
35
|
-
# Deletes users from Keycloak based on a search string.
|
36
|
-
#
|
37
|
-
# @param search_string [String] A string to search for users (e.g., username, email, etc.).
|
38
|
-
# @return [void]
|
39
|
-
# @raise [KeycloakRuby::Error] If any user deletion fails.
|
40
|
-
def self.delete: (untyped search_string) -> untyped
|
41
|
-
|
42
|
-
# Deletes a user from Keycloak by ID.
|
43
|
-
#
|
44
|
-
# @param user_id [String] The ID of the user to delete.
|
45
|
-
# @return [void]
|
46
|
-
# @raise [KeycloakRuby::Error] If the deletion fails.
|
47
|
-
def self.delete_by_id: (untyped user_id) -> untyped
|
48
|
-
|
49
|
-
# Finds users in Keycloak based on a search string.
|
50
|
-
#
|
51
|
-
# @param search_string [String] A string to search for users (e.g., username, email, etc.).
|
52
|
-
# @return [Array<Hash>] An array of user objects (hashes) matching the search criteria.
|
53
|
-
# @raise [KeycloakRuby::Error] If the search fails.
|
54
|
-
def self.find: (untyped search_string) -> untyped
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
# Provides a singleton instance of the KeycloakRuby::Client.
|
59
|
-
#
|
60
|
-
# @return [KeycloakRuby::Client] The client instance used for making API requests.
|
61
|
-
def self.client: () -> untyped
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
module KeycloakRuby
|
66
|
-
# Responsible for performing HTTP requests with HTTParty
|
67
|
-
# and validating the response. This class helps to reduce
|
68
|
-
# FeatureEnvy and keep the Client code cleaner.
|
69
|
-
# :reek:FeatureEnvy
|
70
|
-
class RequestPerformer
|
71
|
-
@config: untyped
|
72
|
-
|
73
|
-
def initialize: (untyped config) -> void
|
74
|
-
|
75
|
-
# Executes an HTTP request and verifies the response code.
|
76
|
-
#
|
77
|
-
# @param request_params [KeycloakRuby::RequestParams] - an object containing
|
78
|
-
# :http_method, :url, :headers, :body, :success_codes, :error_class, :error_message
|
79
|
-
#
|
80
|
-
# @return [HTTParty::Response] The HTTParty response object on success.
|
81
|
-
# @raise [request_params.error_class] If the response code is not in success_codes
|
82
|
-
# or HTTParty raises an error.
|
83
|
-
def call: (untyped request_params) -> untyped
|
84
|
-
|
85
|
-
private
|
86
|
-
|
87
|
-
# Safe validation: returns true/false
|
88
|
-
def verify_response: (untyped response, untyped request_params) -> untyped
|
89
|
-
|
90
|
-
# Bang version that raises an error on invalid response
|
91
|
-
def verify_response!: (untyped response, untyped request_params) -> (nil | untyped)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# keycloak_ruby/testing/keycloak_helpers.rb
|
96
|
-
# :reek:UtilityFunction :reek:ControlParameter :reek:ManualDispatch :reek:BooleanParameter :reek:LongParameterList
|
97
|
-
module KeycloakRuby
|
98
|
-
module Testing
|
99
|
-
# Helper module for tests with Keycloak
|
100
|
-
module KeycloakHelpers
|
101
|
-
self.@keycloak_users: untyped
|
102
|
-
|
103
|
-
# Combines both sign-in approaches with automatic detection of test type
|
104
|
-
def sign_in: (untyped user, ?test_type: untyped) -> untyped
|
105
|
-
|
106
|
-
# Мокирует авторизацию в request-тестах, подставляя указанного пользователя в current_user.
|
107
|
-
# Нужно, так как в request-тестах нет прямого доступа к сессиям и внешним сервисам авторизации.
|
108
|
-
def mock_token_service: (untyped user) -> untyped
|
109
|
-
|
110
|
-
def create_keycloak_user: (username: untyped, email: untyped, password: untyped, temporary: untyped) -> untyped
|
111
|
-
|
112
|
-
# Delete all users from Keycloak
|
113
|
-
def self.delete_all_keycloak_users: () -> untyped
|
114
|
-
|
115
|
-
def self.track_keycloak_user: (untyped user_id) -> untyped
|
116
|
-
|
117
|
-
def self.cleanup_keycloak_users: () -> (nil | untyped)
|
118
|
-
|
119
|
-
private
|
120
|
-
|
121
|
-
def mock_keycloak_login: (untyped user, ?use_capybara: bool) -> untyped
|
122
|
-
|
123
|
-
def capybara_login: () -> untyped
|
124
|
-
|
125
|
-
def generate_fake_tokens: (untyped user) -> untyped
|
126
|
-
|
127
|
-
def store_session: (untyped credentials) -> untyped
|
128
|
-
|
129
|
-
def rspec_auto_detect_test_type: () -> (:request | :feature | :controller)
|
130
|
-
|
131
|
-
def auto_detect_test_type: () -> (untyped | :feature)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
module Minitest
|
137
|
-
class Test
|
138
|
-
include KeycloakRuby::Testing::KeycloakHelpers
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
module KeycloakRuby
|
143
|
-
# Validates Keycloak API responses with both safe and strict modes
|
144
|
-
#
|
145
|
-
# Provides two validation approaches:
|
146
|
-
# 1. Safe validation (#validate) - returns boolean
|
147
|
-
# 2. Strict validation (#validate!) - raises detailed exceptions
|
148
|
-
#
|
149
|
-
# @example Safe validation
|
150
|
-
# validator = ResponseValidator.new(response)
|
151
|
-
# if validator.validate
|
152
|
-
# # proceed with valid response
|
153
|
-
# else
|
154
|
-
# # handle invalid response
|
155
|
-
# end
|
156
|
-
#
|
157
|
-
# @example Strict validation
|
158
|
-
# begin
|
159
|
-
# data = ResponseValidator.new(response).validate!
|
160
|
-
# # use validated data
|
161
|
-
# rescue KeycloakRuby::Errors::TokenRefreshFailed => e
|
162
|
-
# # handle error
|
163
|
-
# end
|
164
|
-
class ResponseValidator
|
165
|
-
@response: untyped
|
166
|
-
|
167
|
-
@data: untyped
|
168
|
-
|
169
|
-
# Initialize with the HTTP response
|
170
|
-
# @param response [HTTP::Response] The raw HTTP response from Keycloak
|
171
|
-
def initialize: (untyped response) -> void
|
172
|
-
|
173
|
-
# Safe validation - returns boolean instead of raising exceptions
|
174
|
-
# @return [Boolean] true if response is valid, false otherwise
|
175
|
-
def validate: () -> (false | untyped)
|
176
|
-
|
177
|
-
# Strict validation - raises exceptions for invalid responses
|
178
|
-
# @return [Hash] Parsed response data if valid
|
179
|
-
# @raise [KeycloakRuby::Errors::TokenRefreshFailed] if validation fails
|
180
|
-
def validate!: () -> untyped
|
181
|
-
|
182
|
-
private
|
183
|
-
|
184
|
-
# Parses JSON response body, returns empty hash on failure
|
185
|
-
# @return [Hash]
|
186
|
-
def parse_response_body: () -> untyped
|
187
|
-
|
188
|
-
# Checks if HTTP status indicates success
|
189
|
-
# @return [Boolean]
|
190
|
-
def valid_http_status?: () -> untyped
|
191
|
-
|
192
|
-
# Checks for OAuth2 "invalid_grant" error
|
193
|
-
# @return [Boolean]
|
194
|
-
def invalid_grant?: () -> untyped
|
195
|
-
|
196
|
-
# Checks for any error in response
|
197
|
-
# @return [Boolean]
|
198
|
-
def error_present?: () -> untyped
|
199
|
-
|
200
|
-
# Verifies access token presence
|
201
|
-
# @return [Boolean]
|
202
|
-
def access_token_present?: () -> untyped
|
203
|
-
|
204
|
-
# Raises appropriate validation error based on failure reason
|
205
|
-
# @raise [KeycloakRuby::Errors::TokenRefreshFailed]
|
206
|
-
def raise_validation_error: () -> untyped
|
207
|
-
|
208
|
-
# Extracts error message from response
|
209
|
-
# @return [String]
|
210
|
-
def extract_error_message: () -> (untyped | untyped | "See response body for details")
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
# lib/keycloak_ruby/config.rb
|
215
|
-
module KeycloakRuby
|
216
|
-
# Configuration class for Keycloak Ruby gem.
|
217
|
-
#
|
218
|
-
# Handles loading and validation of Keycloak configuration from either:
|
219
|
-
# - A YAML file (default: config/keycloak.yml)
|
220
|
-
# - Direct attribute assignment
|
221
|
-
#
|
222
|
-
# == Example YAML Configuration
|
223
|
-
#
|
224
|
-
# development:
|
225
|
-
# keycloak_url: "https://keycloak.example.com"
|
226
|
-
# app_host: "http://localhost:3000"
|
227
|
-
# realm: "my-realm"
|
228
|
-
# oauth_client_id: "my-client"
|
229
|
-
# oauth_client_secret: "secret"
|
230
|
-
#
|
231
|
-
# == Example Programmatic Configuration
|
232
|
-
#
|
233
|
-
# config = KeycloakRuby::Config.new
|
234
|
-
# config.keycloak_url = "https://keycloak.example.com"
|
235
|
-
# config.realm = "my-realm"
|
236
|
-
# # ... etc
|
237
|
-
# :reek:MissingSafeMethod
|
238
|
-
class Config
|
239
|
-
@config_path: untyped
|
240
|
-
|
241
|
-
# Default path to configuration file (Rails.root/config/keycloak.yml)
|
242
|
-
DEFAULT_CONFIG_PATH: untyped
|
243
|
-
|
244
|
-
REQUIRED_ATTRIBUTES: ::Array[:keycloak_url | :app_host | :realm | :oauth_client_id | :oauth_client_secret]
|
245
|
-
|
246
|
-
# :reek:Attribute
|
247
|
-
attr_accessor keycloak_url: untyped
|
248
|
-
|
249
|
-
# :reek:Attribute
|
250
|
-
attr_accessor app_host: untyped
|
251
|
-
|
252
|
-
# :reek:Attribute
|
253
|
-
attr_accessor realm: untyped
|
254
|
-
|
255
|
-
# :reek:Attribute
|
256
|
-
attr_accessor admin_client_id: untyped
|
257
|
-
|
258
|
-
# :reek:Attribute
|
259
|
-
attr_accessor admin_client_secret: untyped
|
260
|
-
|
261
|
-
# :reek:Attribute
|
262
|
-
attr_accessor oauth_client_id: untyped
|
263
|
-
|
264
|
-
# :reek:Attribute
|
265
|
-
attr_accessor oauth_client_secret: untyped
|
266
|
-
|
267
|
-
attr_reader config_path: untyped
|
268
|
-
|
269
|
-
# Initialize configuration, optionally loading from YAML file
|
270
|
-
#
|
271
|
-
# @param config_path [String] Path to YAML config file (default: config/keycloak.yml)
|
272
|
-
#
|
273
|
-
#
|
274
|
-
def initialize: (?untyped config_path) -> void
|
275
|
-
|
276
|
-
def validate!: () -> untyped
|
277
|
-
|
278
|
-
def realm_url: () -> ::String
|
279
|
-
|
280
|
-
def redirect_url: () -> ::String
|
281
|
-
|
282
|
-
def logout_url: () -> ::String
|
283
|
-
|
284
|
-
def token_url: () -> ::String
|
285
|
-
|
286
|
-
private
|
287
|
-
|
288
|
-
# Loads configuration from YAML file if it exists
|
289
|
-
# :reek:ManualDispatch
|
290
|
-
def load_config: () -> (nil | untyped)
|
291
|
-
|
292
|
-
def load_yaml_file: () -> untyped
|
293
|
-
|
294
|
-
def current_env: () -> untyped
|
295
|
-
|
296
|
-
def apply_config: (untyped config_hash) -> untyped
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
module KeycloakRuby
|
301
|
-
# Include test methods
|
302
|
-
module Testing
|
303
|
-
def self.included: (untyped base) -> untyped
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# lib/keycloak_ruby/request_params.rb
|
308
|
-
module KeycloakRuby
|
309
|
-
# A small, typed struct for request parameters
|
310
|
-
RequestParams: untyped
|
311
|
-
end
|
312
|
-
|
313
|
-
# lib/keycloak_ruby/errors.rb
|
314
|
-
module KeycloakRuby
|
315
|
-
# Namespace for all KeycloakRuby specific errors
|
316
|
-
# Follows a hierarchical structure for better error handling
|
317
|
-
module Errors
|
318
|
-
# Base error class for all KeycloakRuby errors
|
319
|
-
# All custom errors inherit from this class
|
320
|
-
class Error < StandardError
|
321
|
-
end
|
322
|
-
|
323
|
-
# Raised when there's an issue with gem configuration
|
324
|
-
class ConfigurationError < Error
|
325
|
-
end
|
326
|
-
|
327
|
-
# Base class for authentication failures
|
328
|
-
class AuthenticationError < Error
|
329
|
-
end
|
330
|
-
|
331
|
-
# Raised when user credentials are invalid
|
332
|
-
class InvalidCredentials < AuthenticationError
|
333
|
-
end
|
334
|
-
|
335
|
-
# Raised when user account is not found
|
336
|
-
class UserNotFound < AuthenticationError
|
337
|
-
end
|
338
|
-
|
339
|
-
# Raised when account is temporarily locked
|
340
|
-
class AccountLocked < AuthenticationError
|
341
|
-
end
|
342
|
-
|
343
|
-
# Raised when user creation fails
|
344
|
-
class UserCreationError < Error
|
345
|
-
end
|
346
|
-
|
347
|
-
# Raised when user update fails
|
348
|
-
class UserUpdateError < Error
|
349
|
-
end
|
350
|
-
|
351
|
-
# Raised when user deletion fails
|
352
|
-
class UserDeletionError < Error
|
353
|
-
end
|
354
|
-
|
355
|
-
# Base class for all token-related errors
|
356
|
-
class TokenError < Error
|
357
|
-
end
|
358
|
-
|
359
|
-
# Raised when token has expired +
|
360
|
-
class TokenExpired < TokenError
|
361
|
-
end
|
362
|
-
|
363
|
-
# Raised when token is invalid (malformed, wrong signature, etc.)
|
364
|
-
class TokenInvalid < TokenError
|
365
|
-
end
|
366
|
-
|
367
|
-
# Raised when token refresh fails
|
368
|
-
class TokenRefreshFailed < TokenError
|
369
|
-
end
|
370
|
-
|
371
|
-
# Raised when token verification fails
|
372
|
-
class TokenVerificationFailed < TokenError
|
373
|
-
end
|
374
|
-
|
375
|
-
# Raised when API request fails
|
376
|
-
class APIError < Error
|
377
|
-
end
|
378
|
-
|
379
|
-
# Raised when receiving 4xx responses from Keycloak
|
380
|
-
class ClientError < APIError
|
381
|
-
end
|
382
|
-
|
383
|
-
# Raised when receiving 5xx responses from Keycloak
|
384
|
-
class ServerError < APIError
|
385
|
-
end
|
386
|
-
|
387
|
-
# Raised when connection to Keycloak fails
|
388
|
-
class ConnectionError < APIError
|
389
|
-
end
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
# lib/keycloak_ruby/token_service.rb
|
394
|
-
# :reek:FeatureEnvy
|
395
|
-
module KeycloakRuby
|
396
|
-
# Service for check and refresh jwt tokens
|
397
|
-
class TokenService
|
398
|
-
@session: untyped
|
399
|
-
|
400
|
-
@config: untyped
|
401
|
-
|
402
|
-
@refresh_mutex: untyped
|
403
|
-
|
404
|
-
@fetch_jwks: untyped
|
405
|
-
|
406
|
-
@issuer_url: untyped
|
407
|
-
|
408
|
-
def initialize: (untyped session, ?untyped config) -> void
|
409
|
-
|
410
|
-
# Finds user by token claims
|
411
|
-
# @return [User, nil]
|
412
|
-
def find_user: () -> (nil | untyped)
|
413
|
-
|
414
|
-
# Store token
|
415
|
-
def store_tokens: (untyped data) -> untyped
|
416
|
-
|
417
|
-
def clear_tokens: () -> untyped
|
418
|
-
|
419
|
-
private
|
420
|
-
|
421
|
-
# It's necessary, because omniauth return request.env["omniauth.auth"] as 'token', not 'access_token'
|
422
|
-
def extract_access_token: (untyped data) -> untyped
|
423
|
-
|
424
|
-
# Gets current token or attempts refresh if expired
|
425
|
-
# @return [Hash, nil] Decoded token claims
|
426
|
-
def current_token: () -> untyped
|
427
|
-
|
428
|
-
# Decodes JWT token
|
429
|
-
# @raise [Errors::TokenExpired, Errors::TokenInvalid]
|
430
|
-
def decode_token: (untyped token) -> untyped
|
431
|
-
|
432
|
-
def fetch_jwks: () -> untyped
|
433
|
-
|
434
|
-
# Attempts to refresh the current token
|
435
|
-
def refresh_current_token: () -> untyped
|
436
|
-
|
437
|
-
# JWT decoding options with JWKS
|
438
|
-
def jwt_decode_options: () -> { algorithms: ::Array["RS256"], verify_iss: true, iss: untyped, aud: untyped, verify_expiration: true, jwks: untyped }
|
439
|
-
|
440
|
-
# Constructs issuer URL from configuration
|
441
|
-
def issuer_url: () -> untyped
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
# lib/keycloak_ruby/token_refresher.rb`
|
446
|
-
module KeycloakRuby
|
447
|
-
# Handles OAuth2 refresh token flow with Keycloak
|
448
|
-
#
|
449
|
-
# Responsible for:
|
450
|
-
# - Executing refresh token requests
|
451
|
-
# - Validating responses
|
452
|
-
# - Managing refresh failures
|
453
|
-
#
|
454
|
-
# @example Basic usage
|
455
|
-
# refresher = TokenRefresher.new(session, config)
|
456
|
-
# new_tokens = refresher.call
|
457
|
-
#
|
458
|
-
# @example With error handling
|
459
|
-
# begin
|
460
|
-
# refresher.call
|
461
|
-
# rescue KeycloakRuby::Errors::TokenRefreshFailed => e
|
462
|
-
# # Handle token refresh failure (e.g., redirect to login)
|
463
|
-
# end
|
464
|
-
class TokenRefresher
|
465
|
-
@session: untyped
|
466
|
-
|
467
|
-
@config: untyped
|
468
|
-
|
469
|
-
def initialize: (untyped session, untyped config) -> void
|
470
|
-
|
471
|
-
# Main entry point - refreshes the token
|
472
|
-
# @return [Hash] New token data if successful
|
473
|
-
# @raise [KeycloakRuby::Errors::TokenRefreshFailed] if refresh fails
|
474
|
-
def call: () -> untyped
|
475
|
-
|
476
|
-
private
|
477
|
-
|
478
|
-
def refresh_token_flow: () -> untyped
|
479
|
-
|
480
|
-
def request_refresh: () -> untyped
|
481
|
-
|
482
|
-
def validate_response: (untyped response) -> untyped
|
483
|
-
|
484
|
-
def handle_http_error: (untyped exception) -> untyped
|
485
|
-
|
486
|
-
# :reek:FeatureEnvy
|
487
|
-
def handle_failed_validation: (untyped response) -> untyped
|
488
|
-
|
489
|
-
def refresh_params: () -> { grant_type: "refresh_token", client_id: untyped, client_secret: untyped, refresh_token: untyped }
|
490
|
-
|
491
|
-
def headers: () -> { "Content-Type" => "application/x-www-form-urlencoded", "Accept" => "application/json", "User-Agent" => ::String }
|
492
|
-
|
493
|
-
def log_refresh_attempt: () -> untyped
|
494
|
-
|
495
|
-
def log_successful_refresh: () -> untyped
|
496
|
-
end
|
497
|
-
end
|
498
|
-
|
499
|
-
# lib/keycloak_ruby/client.rb
|
500
|
-
module KeycloakRuby
|
501
|
-
# Client for interacting with Keycloak (create, delete, and find users, etc.).
|
502
|
-
# rubocop:disable Metrics/ClassLength
|
503
|
-
# rubocop:disable Metrics/MethodLength
|
504
|
-
# :reek:TooManyMethods
|
505
|
-
class Client
|
506
|
-
@config: untyped
|
507
|
-
|
508
|
-
@request_performer: untyped
|
509
|
-
|
510
|
-
def initialize: (?untyped config) -> void
|
511
|
-
|
512
|
-
# Authenticates a user with Keycloak and returns token data upon success.
|
513
|
-
#
|
514
|
-
# @param username [String] The user's username or email.
|
515
|
-
# @param password [String] The user's password.
|
516
|
-
# @return [Hash] The token data (access_token, refresh_token, id_token, etc.).
|
517
|
-
# @raise [KeycloakRuby::Errors::InvalidCredentials] If authentication fails.
|
518
|
-
def authenticate_user: (username: untyped, password: untyped) -> untyped
|
519
|
-
|
520
|
-
# Creates a user in Keycloak and returns the newly created user's data.
|
521
|
-
#
|
522
|
-
# @param user_attrs [Hash] A hash of user attributes. Must contain:
|
523
|
-
# :username, :email, :password, :temporary
|
524
|
-
# @option user_attrs [String] :username The username for the new user.
|
525
|
-
# @option user_attrs [String] :email The user's email.
|
526
|
-
# @option user_attrs [String] :password The initial password.
|
527
|
-
# @option user_attrs [Boolean] :temporary Whether to force a password update on first login.
|
528
|
-
#
|
529
|
-
# @raise [KeycloakRuby::Errors::UserCreationError] If user creation fails.
|
530
|
-
# @return [Hash] The newly created user's data.
|
531
|
-
def create_user: (?::Hash[untyped, untyped] user_attrs) -> untyped
|
532
|
-
|
533
|
-
# Deletes all users that match the provided search string (e.g., username, email).
|
534
|
-
#
|
535
|
-
# @param search_string [String] The search criteria for finding users in Keycloak.
|
536
|
-
# @raise [KeycloakRuby::Errors::UserDeletionError] If any user deletion fails.
|
537
|
-
def delete_users: (untyped search_string) -> untyped
|
538
|
-
|
539
|
-
# Deletes a single user by ID in Keycloak.
|
540
|
-
#
|
541
|
-
# @param user_id [String] The ID of the user to delete.
|
542
|
-
# @raise [KeycloakRuby::Errors::UserDeletionError] If the deletion fails.
|
543
|
-
def delete_user_by_id: (untyped user_id) -> untyped
|
544
|
-
|
545
|
-
# Finds all users in Keycloak that match the given search string.
|
546
|
-
#
|
547
|
-
# @param search [String] The search query (e.g., part of username or email).
|
548
|
-
# @return [Array<Hash>] An array of user objects.
|
549
|
-
# @raise [KeycloakRuby::Errors::APIError] If the request fails.
|
550
|
-
def find_users: (untyped search) -> untyped
|
551
|
-
|
552
|
-
# Updates the redirect URIs for a specific client in Keycloak.
|
553
|
-
#
|
554
|
-
# @param client_id [String] The client ID in Keycloak.
|
555
|
-
# @param redirect_uris [Array<String>] A list of valid redirect URIs for this client.
|
556
|
-
# @raise [KeycloakRuby::Errors::ConnectionError] If the update request fails.
|
557
|
-
def update_client_redirect_uris: (client_id: untyped, redirect_uris: untyped) -> untyped
|
558
|
-
|
559
|
-
private
|
560
|
-
|
561
|
-
def build_auth_body: (untyped username, untyped password) -> { client_id: untyped, client_secret: untyped, username: untyped, password: untyped, grant_type: "password" }
|
562
|
-
|
563
|
-
def build_user_data: (untyped attrs) -> { username: untyped, email: untyped, enabled: true, credentials: ::Array[{ type: "password", value: untyped, temporary: untyped }] }
|
564
|
-
|
565
|
-
# Builds RequestParams and passes them to the RequestPerformer.
|
566
|
-
def http_request: (?::Hash[untyped, untyped] options) -> untyped
|
567
|
-
|
568
|
-
def build_request_params: (untyped opts) -> untyped
|
569
|
-
|
570
|
-
# Retrieves client details by its "clientId".
|
571
|
-
#
|
572
|
-
# @param client_id [String] The "clientId" in Keycloak.
|
573
|
-
# @return [Hash] The client details.
|
574
|
-
# @raise [KeycloakRuby::Errors::ClientError] If no matching client is found or the request fails.
|
575
|
-
def find_client_by_id: (untyped client_id) -> untyped
|
576
|
-
|
577
|
-
# Performs a PUT request to update the redirect URIs for a given client in Keycloak.
|
578
|
-
#
|
579
|
-
# @param client_id [String] The internal Keycloak client ID.
|
580
|
-
# @param redirect_uris [Array<String>] List of valid redirect URIs for this client.
|
581
|
-
# @raise [KeycloakRuby::Errors::ConnectionError] If the update request fails.
|
582
|
-
def update_redirect_uris_for: (untyped client_id, untyped redirect_uris) -> untyped
|
583
|
-
|
584
|
-
# Fetches a user by ID from Keycloak.
|
585
|
-
#
|
586
|
-
# @param user_id [String] The user's ID in Keycloak.
|
587
|
-
# @return [Hash] The Keycloak user data.
|
588
|
-
# @raise [KeycloakRuby::Errors::UserNotFound] If the user cannot be found or the request fails.
|
589
|
-
def fetch_user: (untyped user_id) -> untyped
|
590
|
-
|
591
|
-
# Retrieves the admin token used to authenticate calls to the Keycloak Admin API.
|
592
|
-
#
|
593
|
-
# @return [String] The admin access token.
|
594
|
-
# @raise [KeycloakRuby::Errors::TokenVerificationFailed] If the token request fails.
|
595
|
-
def admin_token: () -> untyped
|
596
|
-
|
597
|
-
# Returns a set of default headers for requests requiring the admin token.
|
598
|
-
#
|
599
|
-
# @return [Hash] A headers Hash including Authorization and Content-Type.
|
600
|
-
def default_headers: () -> { "Authorization" => ::String, "Content-Type" => "application/json" }
|
601
|
-
end
|
602
|
-
end
|
603
|
-
|
604
|
-
# lib/keycloak_ruby/version.rb
|
605
|
-
# Module for interacting with Keycloak
|
606
|
-
module KeycloakRuby
|
607
|
-
# Version module following Semantic Versioning 2.0 guidelines
|
608
|
-
# Provides detailed version information and helper methods
|
609
|
-
#
|
610
|
-
# @example Getting version information
|
611
|
-
# KeycloakRuby::Version::VERSION # => "0.1.0"
|
612
|
-
# KeycloakRuby::Version.to_a # => [0, 1, 0]
|
613
|
-
# KeycloakRuby::Version.to_h # => { major: 0, minor: 1, patch: 0, pre: nil }
|
614
|
-
# KeycloakRuby.version # => "0.1.0"
|
615
|
-
#
|
616
|
-
# @example Checking version
|
617
|
-
# KeycloakRuby::Version >= '0.1.0' # => true
|
618
|
-
# Module for work with Version
|
619
|
-
module Version
|
620
|
-
# Major version number (incompatible API changes)
|
621
|
-
MAJOR: 0
|
622
|
-
|
623
|
-
# Minor version number (backwards-compatible functionality)
|
624
|
-
MINOR: 1
|
625
|
-
|
626
|
-
# Patch version number (backwards-compatible bug fixes)
|
627
|
-
PATCH: 0
|
628
|
-
|
629
|
-
# Pre-release version (nil for stable releases)
|
630
|
-
PRE: nil
|
631
|
-
|
632
|
-
# Full version string
|
633
|
-
VERSION: untyped
|
634
|
-
|
635
|
-
# Returns version components as an array
|
636
|
-
# @return [Array<Integer, Integer, Integer, String|nil>]
|
637
|
-
def self.to_a: () -> ::Array[untyped]
|
30
|
+
private
|
638
31
|
|
639
|
-
|
640
|
-
# @return [Hash<Symbol, Integer|String|nil>]
|
641
|
-
def self.to_h: () -> { major: untyped, minor: untyped, patch: untyped, pre: untyped }
|
32
|
+
def self.resolve_logger: () -> untyped
|
642
33
|
|
643
|
-
|
644
|
-
# @param version_string [String] version to compare with (e.g., "1.2.3")
|
645
|
-
# @return [Boolean]
|
646
|
-
def self.>=: (untyped version_string) -> untyped
|
34
|
+
def self.rails_defined?: () -> untyped
|
647
35
|
|
648
|
-
|
649
|
-
# @return [String]
|
650
|
-
def self.to_s: () -> untyped
|
651
|
-
end
|
36
|
+
def self.rails_logger: () -> untyped
|
652
37
|
|
653
|
-
|
654
|
-
# @return [String]
|
655
|
-
def self.version: () -> untyped
|
38
|
+
def self.default_logger: () -> untyped
|
656
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keycloak_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Arkhipov
|
8
8
|
- Georgy Shcherbakov
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.8'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: omniauth-rails_csrf_protection
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.0.2
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.0.2
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: zeitwerk
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -102,6 +116,7 @@ executables: []
|
|
102
116
|
extensions: []
|
103
117
|
extra_rdoc_files: []
|
104
118
|
files:
|
119
|
+
- ".reek.yml"
|
105
120
|
- ".rspec_status"
|
106
121
|
- ".rubocop.yml"
|
107
122
|
- ".ruby-version"
|