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.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Description
2
+
3
+ This library is designed to use Keycloak identification in Rails application
4
+
5
+ ## Installation
6
+
7
+ Add to Gemfile
8
+
9
+ ```bash
10
+ gem "keycloak_ruby", git: "https://github.com/sergey-arkhipov/keycloak_ruby.git"
11
+
12
+ ```
13
+
14
+ Create initializer for omniauth manually or use generator (config/initializers/omniauth.rb)
15
+
16
+ ```bash
17
+ rails generate keycloak_ruby:install
18
+ ```
19
+
20
+ Now under active development, so you need create manually:
21
+
22
+ ```ruby
23
+ # ApplicationController
24
+
25
+ include KeycloakRuby::Authentication
26
+
27
+ # SeesionController
28
+ def login
29
+ render :login, layout: "login"
30
+ end
31
+
32
+ def create
33
+ auth_info = request.env["omniauth.auth"]
34
+ keycloak_jwt_service.store_tokens(auth_info[:credentials])
35
+ user = User.find_by(email: auth_info.dig(:info, :email))
36
+ return destroy unless user&.active?
37
+
38
+ redirect_to root_path, notice: I18n.t("user.auth_success")
39
+ end
40
+
41
+ def destroy
42
+ id_token = session[:id_token]
43
+ keycloak_jwt_service.clear_tokens
44
+ logout_url = "#{KeycloakRuby.config.logout_url}?post_logout_redirect_uri=#{CGI.escape(root_url)}&" \
45
+ "id_token_hint=#{id_token}"
46
+
47
+ redirect_to logout_url, allow_other_host: true
48
+ end
49
+
50
+ # config/routes.rb
51
+ get '/login', to: 'sessions#login', as: :login
52
+ get '/auth/:provider/callback', to: 'sessions#create'
53
+ delete '/logout', to: 'sessions#destroy', as: :logout
54
+
55
+ ```
56
+
57
+ It is assumed that you have a User model in Rails app
58
+
59
+ ## Architecture Overview
60
+
61
+ ### Component Diagram
62
+
63
+ ```mermaid
64
+ flowchart TD
65
+ subgraph Rails_Application["Rails Application"]
66
+ A[Controller] --> B[TokenService]
67
+ B --> C[TokenRefresher]
68
+ C --> D[Keycloak Server]
69
+ D -->|Refresh Token| C
70
+ C -->|New Tokens| B
71
+ B --> E[User Model]
72
+ B --> F[ResponseValidator]
73
+ C --> F
74
+ F --> G[Errors]
75
+ B --> H[JWT Decoder]
76
+ H --> I[JWKS Cache]
77
+ end
78
+
79
+ style A fill:#ff66ff,stroke:#000000,color:#000000
80
+ style B fill:#66b3ff,stroke:#000000,color:#000000
81
+ style C fill:#66b3ff,stroke:#000000,color:#000000
82
+ style D fill:#ff9933,stroke:#000000,color:#000000
83
+ style E fill:#66cc66,stroke:#000000,color:#000000
84
+ style F fill:#66b3ff,stroke:#000000,color:#000000
85
+ style G fill:#ff6666,stroke:#000000,color:#000000
86
+ style H fill:#66b3ff,stroke:#000000,color:#000000
87
+ style I fill:#99ccff,stroke:#000000,color:#000000
88
+
89
+ %% Subgraph styling - transparent with black border only
90
+ style Rails_Application fill:none,stroke:#000000,color:#000000
91
+
92
+ ```
93
+
94
+ ### Authentication Sequence
95
+
96
+ ```mermaid
97
+ sequenceDiagram
98
+ participant C as Controller
99
+ participant TS as TokenService
100
+ participant TR as TokenRefresher
101
+ participant KS as Keycloak Server
102
+ participant RV as ResponseValidator
103
+ participant JD as JWT Decoder
104
+
105
+ C->>TS: authenticate(user_credentials)
106
+ TS->>TR: call()
107
+ TR->>KS: POST /token
108
+ KS-->>TR: token_response
109
+ TR->>RV: validate(response)
110
+ RV-->>TR: validation_result
111
+ alt Valid
112
+ TR-->>TS: new_tokens
113
+ TS->>JD: decode(access_token)
114
+ JD->>TS: claims
115
+ TS-->>C: user
116
+ else Invalid
117
+ RV->>TR: raise error
118
+ TR->>TS: propagate error
119
+ TS-->>C: nil
120
+ end
121
+ ```
122
+
123
+ ### Key Flows
124
+
125
+ 1. **Initial Authentication**:
126
+
127
+ - Controller → TokenService → Keycloak Server
128
+ - Stores tokens in session
129
+
130
+ 2. **Token Refresh**:
131
+
132
+ - TokenService → TokenRefresher → Keycloak Server
133
+ - Automatic when token expires
134
+
135
+ 3. **Access Validation**:
136
+
137
+ - Verifies token signature and claims
138
+ - Checks user existence in local DB
139
+
140
+ 4. **Error Handling**:
141
+ - Clear sessions on invalid tokens
142
+ - Propagates meaningful errors
143
+
144
+ ```
145
+
146
+ ```
147
+
148
+ ## Test
149
+
150
+ For test purpose there mock helper sign_in(user)
151
+
152
+ - add `require "keycloak_ruby/testing/keycloak_helpers"` to keycloak_helper.rb
153
+ - add sign_in in tests `config.include KeycloakRuby::Testing::KeycloakHelpers`
154
+
155
+ ```
156
+
157
+ ```
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |task|
7
+ task.verbose = false
8
+ end
9
+
10
+ require "rubocop/rake_task"
11
+
12
+ RuboCop::RakeTask.new
13
+
14
+ task default: %i[spec rubocop]
data/lib/assets/.keep ADDED
File without changes
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/generators/keycloak_ruby/install_generator.rb
4
+ require "rails/generators"
5
+
6
+ ##
7
+ # Generates the OmniAuth initializer for Keycloak integration
8
+ #
9
+ # Example:
10
+ # rails generate keycloak_ruby:install
11
+ #
12
+ module KeycloakRuby
13
+ ##
14
+ # Rails generator that creates the OmniAuth initializer configuration
15
+ # for Keycloak authentication
16
+ class InstallGenerator < Rails::Generators::Base
17
+ source_root File.expand_path("../../templates", __dir__)
18
+ desc "Creates Keycloak Ruby initializer for OmniAuth configuration"
19
+
20
+ ##
21
+ # Copies the OmniAuth initializer template to the Rails application
22
+ # unless it already exists
23
+ def copy_initializer
24
+ if omniauth_initializer_exists?
25
+ say_status("skipped", "OmniAuth initializer already exists at #{omniauth_initializer_path}", :yellow)
26
+ else
27
+ template "omniauth.rb", "config/initializers/omniauth.rb"
28
+ say_status("created", "OmniAuth initializer at config/initializers/omniauth.rb", :green)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ ##
35
+ # @return [Pathname] full path to the omniauth initializer
36
+ def omniauth_initializer_path
37
+ @omniauth_initializer_path ||= Rails.root.join("config/initializers/omniauth.rb")
38
+ end
39
+
40
+ ##
41
+ # Checks if the omniauth initializer already exists
42
+ # @return [Boolean] true if file exists
43
+ def omniauth_initializer_exists?
44
+ File.exist?(omniauth_initializer_path)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/keycloak_ruby/authentication.rb
4
+ module KeycloakRuby
5
+ # Concern to add methods to ApplicationController
6
+ module Authentication
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_action :authenticate_user!
11
+ end
12
+
13
+ def keycloak_jwt_service
14
+ @keycloak_jwt_service ||= KeycloakRuby::TokenService.new(session)
15
+ end
16
+
17
+ def authenticate_user!
18
+ redirect_to login_path unless current_user&.active?
19
+ end
20
+
21
+ def current_user
22
+ @current_user ||= keycloak_jwt_service.find_user
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/keycloak_ruby/client.rb
4
+ module KeycloakRuby
5
+ # Client for interacting with Keycloak (create, delete, and find users, etc.).
6
+ # rubocop:disable Metrics/ClassLength
7
+ # rubocop:disable Metrics/MethodLength
8
+ # :reek:TooManyMethods
9
+ class Client
10
+ def initialize(config = KeycloakRuby.config)
11
+ @config = config
12
+ @request_performer = RequestPerformer.new(@config)
13
+ end
14
+
15
+ # Authenticates a user with Keycloak and returns token data upon success.
16
+ #
17
+ # @param username [String] The user's username or email.
18
+ # @param password [String] The user's password.
19
+ # @return [Hash] The token data (access_token, refresh_token, id_token, etc.).
20
+ # @raise [KeycloakRuby::Errors::InvalidCredentials] If authentication fails.
21
+ def authenticate_user(username:, password:)
22
+ body = build_auth_body(username, password)
23
+ response = http_request(
24
+ http_method: :post,
25
+ url: @config.token_url,
26
+ body: URI.encode_www_form(body),
27
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" },
28
+ success_codes: [200],
29
+ error_class: KeycloakRuby::Errors::InvalidCredentials,
30
+ error_message: "Failed to authenticate with Keycloak"
31
+ )
32
+ response.parsed_response
33
+ end
34
+
35
+ # Creates a user in Keycloak and returns the newly created user's data.
36
+ #
37
+ # @param user_attrs [Hash] A hash of user attributes. Must contain:
38
+ # :username, :email, :password, :temporary
39
+ # @option user_attrs [String] :username The username for the new user.
40
+ # @option user_attrs [String] :email The user's email.
41
+ # @option user_attrs [String] :password The initial password.
42
+ # @option user_attrs [Boolean] :temporary Whether to force a password update on first login.
43
+ #
44
+ # @raise [KeycloakRuby::Errors::UserCreationError] If user creation fails.
45
+ # @return [Hash] The newly created user's data.
46
+ def create_user(user_attrs = {})
47
+ user_data = build_user_data(user_attrs)
48
+
49
+ response = http_request(
50
+ http_method: :post,
51
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/users",
52
+ headers: default_headers,
53
+ body: user_data.to_json,
54
+ success_codes: [201],
55
+ error_class: KeycloakRuby::Errors::UserCreationError,
56
+ error_message: "Failed to create Keycloak user"
57
+ )
58
+
59
+ # Extract user ID from the "Location" header, then fetch user details
60
+ user_id = response.headers["Location"].split("/").last
61
+ fetch_user(user_id)
62
+ end
63
+
64
+ # Deletes all users that match the provided search string (e.g., username, email).
65
+ #
66
+ # @param search_string [String] The search criteria for finding users in Keycloak.
67
+ # @raise [KeycloakRuby::Errors::UserDeletionError] If any user deletion fails.
68
+ def delete_users(search_string)
69
+ user_ids = find_users(search_string).pluck("id")
70
+ user_ids.each { |id| delete_user_by_id(id) }
71
+ end
72
+
73
+ # Deletes a single user by ID in Keycloak.
74
+ #
75
+ # @param user_id [String] The ID of the user to delete.
76
+ # @raise [KeycloakRuby::Errors::UserDeletionError] If the deletion fails.
77
+ def delete_user_by_id(user_id)
78
+ http_request(
79
+ http_method: :delete,
80
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/users/#{user_id}",
81
+ headers: default_headers,
82
+ success_codes: [204],
83
+ error_class: KeycloakRuby::Errors::UserDeletionError,
84
+ error_message: "Failed to delete Keycloak user with ID #{user_id}"
85
+ )
86
+ end
87
+
88
+ # Finds all users in Keycloak that match the given search string.
89
+ #
90
+ # @param search [String] The search query (e.g., part of username or email).
91
+ # @return [Array<Hash>] An array of user objects.
92
+ # @raise [KeycloakRuby::Errors::APIError] If the request fails.
93
+ def find_users(search)
94
+ response = http_request(
95
+ http_method: :get,
96
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/users/?search=#{search}",
97
+ headers: default_headers,
98
+ success_codes: [200],
99
+ error_class: KeycloakRuby::Errors::APIError,
100
+ error_message: "Failed to get Keycloak users"
101
+ )
102
+ JSON.parse(response.body)
103
+ end
104
+
105
+ # Updates the redirect URIs for a specific client in Keycloak.
106
+ #
107
+ # @param client_id [String] The client ID in Keycloak.
108
+ # @param redirect_uris [Array<String>] A list of valid redirect URIs for this client.
109
+ # @raise [KeycloakRuby::Errors::ConnectionError] If the update request fails.
110
+ def update_client_redirect_uris(client_id:, redirect_uris:)
111
+ client_record = find_client_by_id(client_id)
112
+ update_redirect_uris_for(client_record["id"], redirect_uris)
113
+ end
114
+
115
+ private
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)
128
+ { username: attrs.fetch(:username), email: attrs.fetch(:email), enabled: true,
129
+ credentials: [
130
+ {
131
+ type: "password",
132
+ value: attrs.fetch(:password),
133
+ temporary: attrs.fetch(:temporary, true)
134
+ }
135
+ ] }
136
+ end
137
+
138
+ # Builds RequestParams and passes them to the RequestPerformer.
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)
145
+ RequestParams.new(
146
+ http_method: opts.fetch(:http_method),
147
+ url: opts.fetch(:url),
148
+ headers: opts.fetch(:headers, {}),
149
+ body: opts.fetch(:body, nil),
150
+ success_codes: opts.fetch(:success_codes, [200]),
151
+ error_class: opts.fetch(:error_class, KeycloakRuby::Errors::APIError),
152
+ error_message: opts.fetch(:error_message, "Request failed")
153
+ )
154
+ end
155
+
156
+ # Retrieves client details by its "clientId".
157
+ #
158
+ # @param client_id [String] The "clientId" in Keycloak.
159
+ # @return [Hash] The client details.
160
+ # @raise [KeycloakRuby::Errors::ClientError] If no matching client is found or the request fails.
161
+ def find_client_by_id(client_id)
162
+ response = http_request(
163
+ http_method: :get,
164
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/clients",
165
+ headers: default_headers,
166
+ success_codes: [200],
167
+ error_class: KeycloakRuby::Errors::ClientError,
168
+ error_message: "Failed to fetch clients"
169
+ )
170
+
171
+ clients = JSON.parse(response.body)
172
+ client_record = clients.find { |client| client["clientId"] == client_id }
173
+ raise KeycloakRuby::Errors::ClientError, "Client #{client_id} not found" unless client_record
174
+
175
+ client_record
176
+ end
177
+
178
+ # Performs a PUT request to update the redirect URIs for a given client in Keycloak.
179
+ #
180
+ # @param client_id [String] The internal Keycloak client ID.
181
+ # @param redirect_uris [Array<String>] List of valid redirect URIs for this client.
182
+ # @raise [KeycloakRuby::Errors::ConnectionError] If the update request fails.
183
+ def update_redirect_uris_for(client_id, redirect_uris)
184
+ http_request(
185
+ http_method: :put,
186
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/clients/#{client_id}",
187
+ headers: default_headers,
188
+ body: { redirectUris: redirect_uris }.to_json,
189
+ success_codes: (200..299),
190
+ error_class: KeycloakRuby::Errors::ConnectionError,
191
+ error_message: "Failed to update redirectUris for client #{client_id}"
192
+ )
193
+ end
194
+
195
+ # Fetches a user by ID from Keycloak.
196
+ #
197
+ # @param user_id [String] The user's ID in Keycloak.
198
+ # @return [Hash] The Keycloak user data.
199
+ # @raise [KeycloakRuby::Errors::UserNotFound] If the user cannot be found or the request fails.
200
+ def fetch_user(user_id)
201
+ response = http_request(
202
+ http_method: :get,
203
+ url: "#{@config.keycloak_url}/admin/realms/#{@config.realm}/users/#{user_id}",
204
+ headers: default_headers,
205
+ success_codes: [200],
206
+ error_class: KeycloakRuby::Errors::UserNotFound,
207
+ error_message: "Failed to fetch Keycloak user with ID #{user_id}"
208
+ )
209
+
210
+ response.parsed_response
211
+ end
212
+
213
+ # Retrieves the admin token used to authenticate calls to the Keycloak Admin API.
214
+ #
215
+ # @return [String] The admin access token.
216
+ # @raise [KeycloakRuby::Errors::TokenVerificationFailed] If the token request fails.
217
+ def admin_token
218
+ body = {
219
+ client_id: @config.admin_client_id,
220
+ client_secret: @config.admin_client_secret,
221
+ grant_type: "client_credentials"
222
+ }
223
+
224
+ response = http_request(
225
+ http_method: :post,
226
+ url: "#{@config.keycloak_url}/realms/#{@config.realm}/protocol/openid-connect/token",
227
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" },
228
+ body: URI.encode_www_form(body),
229
+ success_codes: [200],
230
+ error_class: KeycloakRuby::Errors::TokenVerificationFailed,
231
+ error_message: "Failed to get Keycloak admin token"
232
+ )
233
+
234
+ response.parsed_response["access_token"]
235
+ end
236
+
237
+ # Returns a set of default headers for requests requiring the admin token.
238
+ #
239
+ # @return [Hash] A headers Hash including Authorization and Content-Type.
240
+ def default_headers
241
+ {
242
+ "Authorization" => "Bearer #{admin_token}",
243
+ "Content-Type" => "application/json"
244
+ }
245
+ end
246
+ end
247
+ # rubocop:enable Metrics/ClassLength
248
+ # rubocop:enable Metrics/MethodLength
249
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/keycloak_ruby/config.rb
4
+ module KeycloakRuby
5
+ # Configuration class for Keycloak Ruby gem.
6
+ #
7
+ # Handles loading and validation of Keycloak configuration from either:
8
+ # - A YAML file (default: config/keycloak.yml)
9
+ # - Direct attribute assignment
10
+ #
11
+ # == Example YAML Configuration
12
+ #
13
+ # development:
14
+ # keycloak_url: "https://keycloak.example.com"
15
+ # app_host: "http://localhost:3000"
16
+ # realm: "my-realm"
17
+ # oauth_client_id: "my-client"
18
+ # oauth_client_secret: "secret"
19
+ #
20
+ # == Example Programmatic Configuration
21
+ #
22
+ # config = KeycloakRuby::Config.new
23
+ # config.keycloak_url = "https://keycloak.example.com"
24
+ # config.realm = "my-realm"
25
+ # # ... etc
26
+ # :reek:MissingSafeMethod
27
+ class Config
28
+ # Default path to configuration file (Rails.root/config/keycloak.yml)
29
+ DEFAULT_CONFIG_PATH = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
30
+ Rails.root.join("config", "keycloak.yml")
31
+ else
32
+ # Fallback for non-Rails or when Rails isn't loaded yet
33
+ File.expand_path("config/keycloak.yml", Dir.pwd)
34
+ end
35
+ REQUIRED_ATTRIBUTES = %i[
36
+ keycloak_url
37
+ app_host
38
+ realm
39
+ oauth_client_id
40
+ oauth_client_secret
41
+ ].freeze
42
+ # :reek:Attribute
43
+ attr_accessor :keycloak_url,
44
+ :app_host,
45
+ :realm,
46
+ :admin_client_id,
47
+ :admin_client_secret,
48
+ :oauth_client_id,
49
+ :oauth_client_secret
50
+
51
+ attr_reader :config_path
52
+
53
+ # Initialize configuration, optionally loading from YAML file
54
+ #
55
+ # @param config_path [String] Path to YAML config file (default: config/keycloak.yml)
56
+ #
57
+ #
58
+ def initialize(config_path = DEFAULT_CONFIG_PATH)
59
+ @config_path = config_path
60
+ load_config
61
+ end
62
+
63
+ # Validates that all required configuration attributes are present
64
+ #
65
+ # @raise [KeycloakRuby::Errors::ConfigurationError] if any required attribute is missing
66
+ #
67
+
68
+ def validate!
69
+ REQUIRED_ATTRIBUTES.each do |attr|
70
+ value = public_send(attr)
71
+ raise Errors::ConfigurationError, "#{attr} is required" if value.blank?
72
+ end
73
+ end
74
+
75
+ def realm_url
76
+ "#{keycloak_url}/realms/#{realm}"
77
+ end
78
+
79
+ def redirect_url
80
+ "#{app_host}/auth/keycloak/callback"
81
+ end
82
+
83
+ def logout_url
84
+ "#{realm_url}/protocol/openid-connect/logout"
85
+ end
86
+
87
+ def token_url
88
+ "#{realm_url}/protocol/openid-connect/token"
89
+ end
90
+
91
+ private
92
+
93
+ # Loads configuration from YAML file if it exists
94
+ # :reek:ManualDispatch
95
+ def load_config
96
+ return unless File.exist?(@config_path)
97
+
98
+ yaml_content = load_yaml_file
99
+ env_config = yaml_content[current_env] || {}
100
+ apply_config(env_config)
101
+ end
102
+
103
+ def load_yaml_file
104
+ YAML.safe_load(ERB.new(File.read(@config_path)).result, aliases: true)
105
+ end
106
+
107
+ def current_env
108
+ defined?(Rails) ? Rails.env : ENV["APP_ENV"] || "development"
109
+ end
110
+
111
+ def apply_config(config_hash)
112
+ config_hash.each do |key, value|
113
+ setter = :"#{key}="
114
+ public_send(setter, value) if respond_to?(setter)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/keycloak_ruby/errors.rb
4
+ module KeycloakRuby
5
+ # Namespace for all KeycloakRuby specific errors
6
+ # Follows a hierarchical structure for better error handling
7
+ module Errors
8
+ # Base error class for all KeycloakRuby errors
9
+ # All custom errors inherit from this class
10
+ class Error < StandardError; end
11
+
12
+ ## Configuration Related Errors ##
13
+
14
+ # Raised when there's an issue with gem configuration
15
+ class ConfigurationError < Error; end
16
+
17
+ ## Authentication Related Errors ##
18
+
19
+ # Base class for authentication failures
20
+ class AuthenticationError < Error; end
21
+
22
+ # Raised when user credentials are invalid
23
+ class InvalidCredentials < AuthenticationError; end
24
+
25
+ # Raised when user account is not found
26
+ class UserNotFound < AuthenticationError; end
27
+
28
+ # Raised when account is temporarily locked
29
+ class AccountLocked < AuthenticationError; end
30
+
31
+ ## User Management Errors ##
32
+
33
+ # Raised when user creation fails
34
+ class UserCreationError < Error; end
35
+
36
+ # Raised when user update fails
37
+ class UserUpdateError < Error; end
38
+
39
+ # Raised when user deletion fails
40
+ class UserDeletionError < Error; end
41
+
42
+ ## Token Related Errors ##
43
+
44
+ # Base class for all token-related errors
45
+ class TokenError < Error; end
46
+
47
+ # Raised when token has expired +
48
+ class TokenExpired < TokenError; end
49
+
50
+ # Raised when token is invalid (malformed, wrong signature, etc.)
51
+ class TokenInvalid < TokenError; end
52
+
53
+ # Raised when token refresh fails
54
+ class TokenRefreshFailed < TokenError; end
55
+
56
+ # Raised when token verification fails
57
+ class TokenVerificationFailed < TokenError; end
58
+
59
+ ## API Communication Errors ##
60
+
61
+ # Raised when API request fails
62
+ class APIError < Error; end
63
+
64
+ # Raised when receiving 4xx responses from Keycloak
65
+ class ClientError < APIError; end
66
+
67
+ # Raised when receiving 5xx responses from Keycloak
68
+ class ServerError < APIError; end
69
+
70
+ # Raised when connection to Keycloak fails
71
+ class ConnectionError < APIError; end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/keycloak_ruby/request_params.rb
4
+ module KeycloakRuby
5
+ # A small, typed struct for request parameters
6
+ RequestParams = Struct.new(
7
+ :http_method,
8
+ :url,
9
+ :headers,
10
+ :body,
11
+ :success_codes,
12
+ :error_class,
13
+ :error_message,
14
+ keyword_init: true
15
+ )
16
+ end