keycloak_ruby 0.1.0
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 +7 -0
- data/.rspec_status +68 -0
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +674 -0
- data/README.md +157 -0
- data/Rakefile +14 -0
- data/lib/assets/.keep +0 -0
- data/lib/generators/keycloak_ruby/install_generator.rb +47 -0
- data/lib/keycloak_ruby/authentication.rb +25 -0
- data/lib/keycloak_ruby/client.rb +249 -0
- data/lib/keycloak_ruby/config.rb +118 -0
- data/lib/keycloak_ruby/errors.rb +73 -0
- data/lib/keycloak_ruby/request_params.rb +16 -0
- data/lib/keycloak_ruby/request_performer.rb +65 -0
- data/lib/keycloak_ruby/response_validator.rb +115 -0
- data/lib/keycloak_ruby/testing/keycloak_helpers.rb +125 -0
- data/lib/keycloak_ruby/testing.rb +13 -0
- data/lib/keycloak_ruby/token_refresher.rb +103 -0
- data/lib/keycloak_ruby/token_service.rb +108 -0
- data/lib/keycloak_ruby/user.rb +55 -0
- data/lib/keycloak_ruby/version.rb +62 -0
- data/lib/keycloak_ruby.rb +65 -0
- data/lib/templates/omniauth.rb +29 -0
- data/sig/keycloak_ruby.rbs +656 -0
- metadata +129 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/request_performer.rb
|
4
|
+
|
5
|
+
module KeycloakRuby
|
6
|
+
# Responsible for performing HTTP requests with HTTParty
|
7
|
+
# and validating the response. This class helps to reduce
|
8
|
+
# FeatureEnvy and keep the Client code cleaner.
|
9
|
+
# :reek:FeatureEnvy
|
10
|
+
class RequestPerformer
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes an HTTP request and verifies the response code.
|
16
|
+
#
|
17
|
+
# @param request_params [KeycloakRuby::RequestParams] - an object containing
|
18
|
+
# :http_method, :url, :headers, :body, :success_codes, :error_class, :error_message
|
19
|
+
#
|
20
|
+
# @return [HTTParty::Response] The HTTParty response object on success.
|
21
|
+
# @raise [request_params.error_class] If the response code is not in success_codes
|
22
|
+
# or HTTParty raises an error.
|
23
|
+
def call(request_params)
|
24
|
+
# To reduce FeatureEnvy, extract local variables
|
25
|
+
http_method = request_params.http_method
|
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)
|
31
|
+
verify_response!(response, request_params)
|
32
|
+
response
|
33
|
+
rescue HTTParty::Error => e
|
34
|
+
KeycloakRuby.logger.error("#{request_params.error_message} (HTTParty error): #{e.message}")
|
35
|
+
raise request_params.error_class, e.message
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Safe validation: returns true/false
|
41
|
+
def verify_response(response, request_params)
|
42
|
+
code = response.code
|
43
|
+
success_codes = request_params.success_codes
|
44
|
+
|
45
|
+
case success_codes
|
46
|
+
when Range
|
47
|
+
success_codes.cover?(code)
|
48
|
+
when Array
|
49
|
+
success_codes.include?(code)
|
50
|
+
else
|
51
|
+
code == success_codes
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Bang version that raises an error on invalid response
|
56
|
+
def verify_response!(response, request_params)
|
57
|
+
return if verify_response(response, request_params)
|
58
|
+
|
59
|
+
code = response.code
|
60
|
+
error_message = request_params.error_message
|
61
|
+
KeycloakRuby.logger.error("#{error_message}: #{code} => #{response.body}")
|
62
|
+
raise request_params.error_class, "#{error_message}: #{code} => #{response.body}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRuby
|
4
|
+
# Validates Keycloak API responses with both safe and strict modes
|
5
|
+
#
|
6
|
+
# Provides two validation approaches:
|
7
|
+
# 1. Safe validation (#validate) - returns boolean
|
8
|
+
# 2. Strict validation (#validate!) - raises detailed exceptions
|
9
|
+
#
|
10
|
+
# @example Safe validation
|
11
|
+
# validator = ResponseValidator.new(response)
|
12
|
+
# if validator.validate
|
13
|
+
# # proceed with valid response
|
14
|
+
# else
|
15
|
+
# # handle invalid response
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @example Strict validation
|
19
|
+
# begin
|
20
|
+
# data = ResponseValidator.new(response).validate!
|
21
|
+
# # use validated data
|
22
|
+
# rescue KeycloakRuby::Errors::TokenRefreshFailed => e
|
23
|
+
# # handle error
|
24
|
+
# end
|
25
|
+
class ResponseValidator
|
26
|
+
# Initialize with the HTTP response
|
27
|
+
# @param response [HTTP::Response] The raw HTTP response from Keycloak
|
28
|
+
def initialize(response)
|
29
|
+
@response = response
|
30
|
+
@data = parse_response_body
|
31
|
+
end
|
32
|
+
|
33
|
+
# Safe validation - returns boolean instead of raising exceptions
|
34
|
+
# @return [Boolean] true if response is valid, false otherwise
|
35
|
+
def validate
|
36
|
+
return false unless valid_http_status?
|
37
|
+
return false if invalid_grant?
|
38
|
+
return false if error_present?
|
39
|
+
|
40
|
+
access_token_present?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Strict validation - raises exceptions for invalid responses
|
44
|
+
# @return [Hash] Parsed response data if valid
|
45
|
+
# @raise [KeycloakRuby::Errors::TokenRefreshFailed] if validation fails
|
46
|
+
def validate!
|
47
|
+
validate or raise_validation_error
|
48
|
+
@data
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Parses JSON response body, returns empty hash on failure
|
54
|
+
# @return [Hash]
|
55
|
+
def parse_response_body
|
56
|
+
JSON.parse(@response.body)
|
57
|
+
rescue JSON::ParserError
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Checks if HTTP status indicates success
|
62
|
+
# @return [Boolean]
|
63
|
+
def valid_http_status?
|
64
|
+
@response.success?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks for OAuth2 "invalid_grant" error
|
68
|
+
# @return [Boolean]
|
69
|
+
def invalid_grant?
|
70
|
+
@data["error"] == "invalid_grant"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Checks for any error in response
|
74
|
+
# @return [Boolean]
|
75
|
+
def error_present?
|
76
|
+
@data.key?("error")
|
77
|
+
end
|
78
|
+
|
79
|
+
# Verifies access token presence
|
80
|
+
# @return [Boolean]
|
81
|
+
def access_token_present?
|
82
|
+
@data["access_token"].present?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Raises appropriate validation error based on failure reason
|
86
|
+
# @raise [KeycloakRuby::Errors::TokenRefreshFailed]
|
87
|
+
def raise_validation_error # rubocop:disable Metrics/MethodLength
|
88
|
+
if !valid_http_status?
|
89
|
+
raise Errors::TokenRefreshFailed,
|
90
|
+
"Keycloak API request failed with status #{@response.code}: #{extract_error_message}"
|
91
|
+
elsif invalid_grant?
|
92
|
+
raise Errors::TokenRefreshFailed,
|
93
|
+
"Invalid grant: #{@data["error_description"] || "Refresh token invalid or expired"}"
|
94
|
+
elsif error_present?
|
95
|
+
raise Errors::TokenRefreshFailed,
|
96
|
+
"Keycloak error: #{@data["error"]} - #{@data["error_description"]}"
|
97
|
+
else
|
98
|
+
raise Errors::TokenRefreshFailed,
|
99
|
+
"Invalid response: access token missing from response"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Extracts error message from response
|
104
|
+
# @return [String]
|
105
|
+
def extract_error_message
|
106
|
+
if @data["error_description"]
|
107
|
+
@data["error_description"]
|
108
|
+
elsif @response.body.length < 100 # Prevent huge error messages
|
109
|
+
@response.body
|
110
|
+
else
|
111
|
+
"See response body for details"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# keycloak_ruby/testing/keycloak_helpers.rb
|
4
|
+
# :reek:UtilityFunction :reek:ControlParameter :reek:ManualDispatch :reek:BooleanParameter :reek:LongParameterList
|
5
|
+
module KeycloakRuby
|
6
|
+
module Testing
|
7
|
+
# Helper module for tests with Keycloak
|
8
|
+
module KeycloakHelpers
|
9
|
+
# Combines both sign-in approaches with automatic detection of test type
|
10
|
+
def sign_in(user, test_type: auto_detect_test_type)
|
11
|
+
case test_type
|
12
|
+
when :request
|
13
|
+
mock_token_service(user)
|
14
|
+
when :feature, :system
|
15
|
+
mock_keycloak_login(user, use_capybara: true)
|
16
|
+
else # :controller, :view, etc.
|
17
|
+
mock_keycloak_login(user, use_capybara: false)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Мокирует авторизацию в request-тестах, подставляя указанного пользователя в current_user.
|
22
|
+
# Нужно, так как в request-тестах нет прямого доступа к сессиям и внешним сервисам авторизации.
|
23
|
+
def mock_token_service(user)
|
24
|
+
token_service_double = instance_double(KeycloakRuby::TokenService, find_user: user)
|
25
|
+
allow(KeycloakRuby::TokenService).to receive(:new).and_return(token_service_double)
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_keycloak_user(username:, email:, password:, temporary:)
|
29
|
+
user_data = KeycloakRuby::User.create(username:, email:, password:, temporary:)
|
30
|
+
KeycloakHelpers.track_keycloak_user(user_data["id"])
|
31
|
+
user_data
|
32
|
+
end
|
33
|
+
|
34
|
+
# Delete all users from Keycloak
|
35
|
+
def self.delete_all_keycloak_users
|
36
|
+
users = KeycloakRuby::User.find("") # Empty search string to find all users
|
37
|
+
users.each do |user|
|
38
|
+
KeycloakRuby::User.delete_by_id(user["id"])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.track_keycloak_user(user_id)
|
43
|
+
@keycloak_users ||= []
|
44
|
+
@keycloak_users << user_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.cleanup_keycloak_users
|
48
|
+
return unless @keycloak_users
|
49
|
+
|
50
|
+
@keycloak_users.each do |user_id|
|
51
|
+
KeycloakRuby::User.delete(user_id)
|
52
|
+
end
|
53
|
+
@keycloak_users.clear
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def mock_keycloak_login(user, use_capybara: true)
|
59
|
+
OmniAuth.config.test_mode = true
|
60
|
+
token_data = generate_fake_tokens(user)
|
61
|
+
OmniAuth.config.mock_auth[:keycloak] = token_data
|
62
|
+
|
63
|
+
if use_capybara
|
64
|
+
capybara_login
|
65
|
+
else
|
66
|
+
store_session(token_data.credentials)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def capybara_login
|
71
|
+
visit "/login"
|
72
|
+
click_on I18n.t("user.login") if page.has_button? I18n.t("user.login")
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_fake_tokens(user)
|
76
|
+
email = user.email
|
77
|
+
token_payload = { "email" => email, "exp" => 2.hours.from_now.to_i }
|
78
|
+
|
79
|
+
OmniAuth::AuthHash.new(provider: "keycloak", uid: "uid-#{email}", info: { email: email },
|
80
|
+
credentials: OmniAuth::AuthHash.new(
|
81
|
+
token: JWT.encode(token_payload, nil, "none"),
|
82
|
+
refresh_token: "fake-refresh-#{email}",
|
83
|
+
id_token: "fake-id-#{email}",
|
84
|
+
expires_at: 2.hours.from_now.to_i
|
85
|
+
))
|
86
|
+
end
|
87
|
+
|
88
|
+
def store_session(credentials)
|
89
|
+
session[:access_token] = credentials[:token]
|
90
|
+
session[:refresh_token] = credentials[:refresh_token]
|
91
|
+
session[:id_token] = credentials[:id_token]
|
92
|
+
end
|
93
|
+
|
94
|
+
def rspec_auto_detect_test_type
|
95
|
+
if RSpec.current_example.metadata[:type] == :request
|
96
|
+
:request
|
97
|
+
elsif defined?(Capybara::DSL) && Capybara.current_driver != :rack_test
|
98
|
+
:feature
|
99
|
+
else
|
100
|
+
:controller
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def auto_detect_test_type
|
105
|
+
if defined?(RSpec) && RSpec.current_example
|
106
|
+
rspec_auto_detect_test_type
|
107
|
+
else
|
108
|
+
:feature
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
# Automatically include in common test frameworks
|
115
|
+
if defined?(RSpec)
|
116
|
+
RSpec.configure do |config|
|
117
|
+
config.include KeycloakRuby::Testing::KeycloakHelpers
|
118
|
+
end
|
119
|
+
elsif defined?(Minitest)
|
120
|
+
module Minitest
|
121
|
+
class Test
|
122
|
+
include KeycloakRuby::Testing::KeycloakHelpers
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/testing.rb
|
4
|
+
require_relative "testing/keycloak_helpers"
|
5
|
+
|
6
|
+
module KeycloakRuby
|
7
|
+
# Include test methods
|
8
|
+
module Testing
|
9
|
+
def self.included(base)
|
10
|
+
base.include KeycloakHelpers
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/token_refresher.rb`
|
4
|
+
module KeycloakRuby
|
5
|
+
# Handles OAuth2 refresh token flow with Keycloak
|
6
|
+
#
|
7
|
+
# Responsible for:
|
8
|
+
# - Executing refresh token requests
|
9
|
+
# - Validating responses
|
10
|
+
# - Managing refresh failures
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# refresher = TokenRefresher.new(session, config)
|
14
|
+
# new_tokens = refresher.call
|
15
|
+
#
|
16
|
+
# @example With error handling
|
17
|
+
# begin
|
18
|
+
# refresher.call
|
19
|
+
# rescue KeycloakRuby::Errors::TokenRefreshFailed => e
|
20
|
+
# # Handle token refresh failure (e.g., redirect to login)
|
21
|
+
# end
|
22
|
+
class TokenRefresher
|
23
|
+
def initialize(session, config)
|
24
|
+
@session = session
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
# Main entry point - refreshes the token
|
29
|
+
# @return [Hash] New token data if successful
|
30
|
+
# @raise [KeycloakRuby::Errors::TokenRefreshFailed] if refresh fails
|
31
|
+
def call
|
32
|
+
refresh_token_flow
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def refresh_token_flow
|
38
|
+
log_refresh_attempt
|
39
|
+
response = request_refresh
|
40
|
+
validate_response(response)
|
41
|
+
rescue HTTParty::Error => e
|
42
|
+
handle_http_error(e)
|
43
|
+
end
|
44
|
+
|
45
|
+
def request_refresh
|
46
|
+
HTTParty.post(
|
47
|
+
@config.token_url,
|
48
|
+
body: refresh_params,
|
49
|
+
headers: headers,
|
50
|
+
timeout: 30 # Add timeout for safety
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_response(response)
|
55
|
+
validator = ResponseValidator.new(response)
|
56
|
+
|
57
|
+
if validator.validate
|
58
|
+
log_successful_refresh
|
59
|
+
validator.validate! # Returns the validated token data
|
60
|
+
else
|
61
|
+
handle_failed_validation(response)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_http_error(exception)
|
66
|
+
error_message = "Token refresh HTTP error: #{exception.message}"
|
67
|
+
KeycloakRuby.logger.error(error_message)
|
68
|
+
raise Errors::TokenRefreshFailed, error_message
|
69
|
+
end
|
70
|
+
|
71
|
+
# :reek:FeatureEnvy
|
72
|
+
def handle_failed_validation(response)
|
73
|
+
error_message = "Token refresh failed. Status: #{response.code}, Body: #{response.body.truncate(200)}"
|
74
|
+
KeycloakRuby.logger.error(error_message)
|
75
|
+
raise Errors::TokenRefreshFailed, error_message
|
76
|
+
end
|
77
|
+
|
78
|
+
def refresh_params
|
79
|
+
{
|
80
|
+
grant_type: "refresh_token",
|
81
|
+
client_id: @config.oauth_client_id,
|
82
|
+
client_secret: @config.oauth_client_secret,
|
83
|
+
refresh_token: @session[:refresh_token]
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def headers
|
88
|
+
{
|
89
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
90
|
+
"Accept" => "application/json",
|
91
|
+
"User-Agent" => "KeycloakRuby/#{KeycloakRuby::Version::VERSION}"
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def log_refresh_attempt
|
96
|
+
KeycloakRuby.logger.info("Attempting token refresh for client: #{@config.oauth_client_id}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def log_successful_refresh
|
100
|
+
KeycloakRuby.logger.info("Successfully refreshed tokens for client: #{@config.oauth_client_id}")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/token_service.rb
|
4
|
+
module KeycloakRuby
|
5
|
+
# Service for check and refresh jwt tokens
|
6
|
+
class TokenService
|
7
|
+
def initialize(session, config = KeycloakRuby::Config.new)
|
8
|
+
@session = session
|
9
|
+
@config = config
|
10
|
+
@refresh_mutex = Mutex.new # Mutex ensures only one refresh happens at a time
|
11
|
+
end
|
12
|
+
|
13
|
+
# Finds user by token claims
|
14
|
+
# @return [User, nil]
|
15
|
+
def find_user
|
16
|
+
claims = current_token or return
|
17
|
+
user = ::User.find_by(email: claims["email"])
|
18
|
+
clear_tokens unless user
|
19
|
+
user
|
20
|
+
end
|
21
|
+
|
22
|
+
# Store token
|
23
|
+
def store_tokens(data)
|
24
|
+
@session[:access_token] = extract_access_token(data)
|
25
|
+
@session[:refresh_token] = data["refresh_token"] if data["refresh_token"]
|
26
|
+
@session[:id_token] = data["id_token"] if data["id_token"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear_tokens
|
30
|
+
%i[access_token refresh_token id_token].each { |token| @session.delete(token) }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# 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
|
+
# Gets current token or attempts refresh if expired
|
41
|
+
# @return [Hash, nil] Decoded token claims
|
42
|
+
def current_token
|
43
|
+
token = @session[:access_token] or return
|
44
|
+
decode_token(token)
|
45
|
+
rescue Errors::TokenExpired # Normal refresh
|
46
|
+
refresh_current_token
|
47
|
+
rescue Errors::TokenInvalid => e # Wrong token refresh
|
48
|
+
KeycloakRuby.logger.error("JWT Error: #{e.message}")
|
49
|
+
clear_tokens
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Decodes JWT token
|
54
|
+
# @raise [Errors::TokenExpired, Errors::TokenInvalid]
|
55
|
+
def decode_token(token)
|
56
|
+
options = jwt_decode_options
|
57
|
+
|
58
|
+
if Rails.env.test?
|
59
|
+
JWT.decode(token, nil, false).first # без проверки подписи в тестах
|
60
|
+
else
|
61
|
+
JWT.decode(token, nil, true, options).first
|
62
|
+
end
|
63
|
+
rescue JWT::ExpiredSignature => e
|
64
|
+
raise Errors::TokenExpired, e.message
|
65
|
+
rescue JWT::DecodeError => e
|
66
|
+
raise Errors::TokenInvalid, e.message
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_jwks
|
70
|
+
realm_url = @config.realm_url
|
71
|
+
@fetch_jwks ||= Rails.cache.fetch("keycloak_jwks", expires_in: 1.hour) do
|
72
|
+
uri = URI("#{realm_url}/protocol/openid-connect/certs")
|
73
|
+
JSON.parse(Net::HTTP.get(uri))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Attempts to refresh the current token
|
78
|
+
def refresh_current_token
|
79
|
+
@refresh_mutex.synchronize do
|
80
|
+
new_tokens = TokenRefresher.new(@session, @config).call
|
81
|
+
store_tokens(new_tokens)
|
82
|
+
decode_token(new_tokens["access_token"])
|
83
|
+
end
|
84
|
+
rescue Errors::TokenRefreshFailed => e
|
85
|
+
KeycloakRuby.logger.error("Refresh failed: #{e.message}")
|
86
|
+
clear_tokens
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# JWT decoding options with JWKS
|
91
|
+
def jwt_decode_options
|
92
|
+
{
|
93
|
+
algorithms: ["RS256"],
|
94
|
+
verify_iss: true,
|
95
|
+
iss: issuer_url,
|
96
|
+
# verify_aud: true, # Вроде рекомендуют, но у нас не работает
|
97
|
+
aud: @config.oauth_client_id,
|
98
|
+
verify_expiration: true,
|
99
|
+
jwks: fetch_jwks
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Constructs issuer URL from configuration
|
104
|
+
def issuer_url
|
105
|
+
@issuer_url ||= @config.realm_url
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/user.rb
|
4
|
+
module KeycloakRuby
|
5
|
+
# User-related operations for interacting with Keycloak.
|
6
|
+
# This class provides a simple interface for creating, deleting, and finding users in Keycloak.
|
7
|
+
class User
|
8
|
+
class << self
|
9
|
+
# Creates a new user in Keycloak.
|
10
|
+
#
|
11
|
+
# @param user_attrs [Hash] A hash of user attributes (e.g., :username, :email, :password, :temporary).
|
12
|
+
# @return [Hash] The created user's data.
|
13
|
+
# @raise [KeycloakRuby::Error] If the user creation fails.
|
14
|
+
def create(user_attrs = {})
|
15
|
+
client.create_user(user_attrs)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Deletes users from Keycloak based on a search string.
|
19
|
+
#
|
20
|
+
# @param search_string [String] A string to search for users (e.g., username, email, etc.).
|
21
|
+
# @return [void]
|
22
|
+
# @raise [KeycloakRuby::Error] If any user deletion fails.
|
23
|
+
def delete(search_string)
|
24
|
+
client.delete_users(search_string)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Deletes a user from Keycloak by ID.
|
28
|
+
#
|
29
|
+
# @param user_id [String] The ID of the user to delete.
|
30
|
+
# @return [void]
|
31
|
+
# @raise [KeycloakRuby::Error] If the deletion fails.
|
32
|
+
def delete_by_id(user_id)
|
33
|
+
client.delete_user_by_id(user_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Finds users in Keycloak based on a search string.
|
37
|
+
#
|
38
|
+
# @param search_string [String] A string to search for users (e.g., username, email, etc.).
|
39
|
+
# @return [Array<Hash>] An array of user objects (hashes) matching the search criteria.
|
40
|
+
# @raise [KeycloakRuby::Error] If the search fails.
|
41
|
+
def find(search_string)
|
42
|
+
client.find_users(search_string)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Provides a singleton instance of the KeycloakRuby::Client.
|
48
|
+
#
|
49
|
+
# @return [KeycloakRuby::Client] The client instance used for making API requests.
|
50
|
+
def client
|
51
|
+
@client ||= KeycloakRuby::Client.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/keycloak_ruby/version.rb
|
4
|
+
# Module for interacting with Keycloak
|
5
|
+
module KeycloakRuby
|
6
|
+
# Version module following Semantic Versioning 2.0 guidelines
|
7
|
+
# Provides detailed version information and helper methods
|
8
|
+
#
|
9
|
+
# @example Getting version information
|
10
|
+
# KeycloakRuby::Version::VERSION # => "0.1.0"
|
11
|
+
# KeycloakRuby::Version.to_a # => [0, 1, 0]
|
12
|
+
# KeycloakRuby::Version.to_h # => { major: 0, minor: 1, patch: 0, pre: nil }
|
13
|
+
# KeycloakRuby.version # => "0.1.0"
|
14
|
+
#
|
15
|
+
# @example Checking version
|
16
|
+
# KeycloakRuby::Version >= '0.1.0' # => true
|
17
|
+
# Module for work with Version
|
18
|
+
module Version
|
19
|
+
# Major version number (incompatible API changes)
|
20
|
+
MAJOR = 0
|
21
|
+
# Minor version number (backwards-compatible functionality)
|
22
|
+
MINOR = 1
|
23
|
+
# Patch version number (backwards-compatible bug fixes)
|
24
|
+
PATCH = 0
|
25
|
+
# Pre-release version (nil for stable releases)
|
26
|
+
PRE = nil
|
27
|
+
|
28
|
+
# Full version string
|
29
|
+
VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".").freeze
|
30
|
+
|
31
|
+
# Returns version components as an array
|
32
|
+
# @return [Array<Integer, Integer, Integer, String|nil>]
|
33
|
+
def self.to_a
|
34
|
+
[MAJOR, MINOR, PATCH, PRE]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns version components as a hash
|
38
|
+
# @return [Hash<Symbol, Integer|String|nil>]
|
39
|
+
def self.to_h
|
40
|
+
{ major: MAJOR, minor: MINOR, patch: PATCH, pre: PRE }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Compares version with another version string
|
44
|
+
# @param version_string [String] version to compare with (e.g., "1.2.3")
|
45
|
+
# @return [Boolean]
|
46
|
+
def self.>=(version_string)
|
47
|
+
Gem::Version.new(VERSION) >= Gem::Version.new(version_string)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the full version string
|
51
|
+
# @return [String]
|
52
|
+
def self.to_s
|
53
|
+
VERSION
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the current gem version
|
58
|
+
# @return [String]
|
59
|
+
def self.version
|
60
|
+
Version::VERSION
|
61
|
+
end
|
62
|
+
end
|