clavis 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.actrc +4 -0
  3. data/.cursor/rules/ruby-gem.mdc +49 -0
  4. data/.gemignore +6 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +88 -0
  7. data/.vscode/settings.json +22 -0
  8. data/CHANGELOG.md +127 -0
  9. data/CODE_OF_CONDUCT.md +3 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +838 -0
  12. data/Rakefile +341 -0
  13. data/UPGRADE.md +57 -0
  14. data/app/assets/stylesheets/clavis.css +133 -0
  15. data/app/controllers/clavis/auth_controller.rb +133 -0
  16. data/config/database.yml +16 -0
  17. data/config/routes.rb +49 -0
  18. data/docs/SECURITY.md +340 -0
  19. data/docs/TESTING.md +78 -0
  20. data/docs/integration.md +272 -0
  21. data/error_handling.md +355 -0
  22. data/file_structure.md +221 -0
  23. data/gemfiles/rails_80.gemfile +17 -0
  24. data/gemfiles/rails_80.gemfile.lock +286 -0
  25. data/implementation_plan.md +523 -0
  26. data/lib/clavis/configuration.rb +196 -0
  27. data/lib/clavis/controllers/concerns/authentication.rb +232 -0
  28. data/lib/clavis/controllers/concerns/session_management.rb +117 -0
  29. data/lib/clavis/engine.rb +191 -0
  30. data/lib/clavis/errors.rb +205 -0
  31. data/lib/clavis/logging.rb +116 -0
  32. data/lib/clavis/models/concerns/oauth_authenticatable.rb +169 -0
  33. data/lib/clavis/oauth_identity.rb +174 -0
  34. data/lib/clavis/providers/apple.rb +135 -0
  35. data/lib/clavis/providers/base.rb +432 -0
  36. data/lib/clavis/providers/custom_provider_example.rb +57 -0
  37. data/lib/clavis/providers/facebook.rb +84 -0
  38. data/lib/clavis/providers/generic.rb +63 -0
  39. data/lib/clavis/providers/github.rb +87 -0
  40. data/lib/clavis/providers/google.rb +98 -0
  41. data/lib/clavis/providers/microsoft.rb +57 -0
  42. data/lib/clavis/security/csrf_protection.rb +79 -0
  43. data/lib/clavis/security/https_enforcer.rb +90 -0
  44. data/lib/clavis/security/input_validator.rb +192 -0
  45. data/lib/clavis/security/parameter_filter.rb +64 -0
  46. data/lib/clavis/security/rate_limiter.rb +109 -0
  47. data/lib/clavis/security/redirect_uri_validator.rb +124 -0
  48. data/lib/clavis/security/session_manager.rb +220 -0
  49. data/lib/clavis/security/token_storage.rb +114 -0
  50. data/lib/clavis/user_info_normalizer.rb +74 -0
  51. data/lib/clavis/utils/nonce_store.rb +14 -0
  52. data/lib/clavis/utils/secure_token.rb +17 -0
  53. data/lib/clavis/utils/state_store.rb +18 -0
  54. data/lib/clavis/version.rb +6 -0
  55. data/lib/clavis/view_helpers.rb +260 -0
  56. data/lib/clavis.rb +132 -0
  57. data/lib/generators/clavis/controller/controller_generator.rb +48 -0
  58. data/lib/generators/clavis/controller/templates/controller.rb.tt +137 -0
  59. data/lib/generators/clavis/controller/templates/views/login.html.erb.tt +145 -0
  60. data/lib/generators/clavis/install_generator.rb +182 -0
  61. data/lib/generators/clavis/templates/add_oauth_to_users.rb +28 -0
  62. data/lib/generators/clavis/templates/clavis.css +133 -0
  63. data/lib/generators/clavis/templates/initializer.rb +47 -0
  64. data/lib/generators/clavis/templates/initializer.rb.tt +76 -0
  65. data/lib/generators/clavis/templates/migration.rb +18 -0
  66. data/lib/generators/clavis/templates/migration.rb.tt +16 -0
  67. data/lib/generators/clavis/user_method/user_method_generator.rb +219 -0
  68. data/lib/tasks/provider_verification.rake +77 -0
  69. data/llms.md +487 -0
  70. data/log/development.log +20 -0
  71. data/log/test.log +0 -0
  72. data/sig/clavis.rbs +4 -0
  73. data/testing_plan.md +710 -0
  74. metadata +258 -0
@@ -0,0 +1,523 @@
1
+ # Clavis - Implementation Plan (Revised)
2
+
3
+ ## Overview
4
+
5
+ Clavis will be a Ruby gem that provides an easy-to-use implementation of OIDC (OpenID Connect) and OAuth2 functionality for Rails applications. It will focus on simplifying the "Sign in with ____" experience while adhering to relevant security standards and best practices.
6
+
7
+ ## Core Requirements
8
+
9
+ 1. **OIDC/OAuth2 Implementation**
10
+ - Support for Authorization Code Flow (primary)
11
+ - Session-based authentication integration with Rails
12
+ - Support for major identity providers (Google, Apple, GitHub, Facebook, Microsoft)
13
+
14
+ 2. **Rails Integration**
15
+ - Idempotent generators for controllers and views
16
+ - Individual view helpers for provider buttons with SVG icons
17
+ - Integration with Rails 8 authentication via concerns
18
+ - Customizable templates and styling
19
+
20
+ 3. **Developer Experience**
21
+ - Sensible defaults for quick implementation
22
+ - Comprehensive documentation
23
+ - Flexible configuration options
24
+ - Configuration validation with helpful errors
25
+
26
+ ## Ideal Developer Workflow
27
+
28
+ 1. Developer has existing Rails 8 application with user authentication
29
+ 2. They install the Clavis gem
30
+ 3. They run generators to set up Clavis infrastructure
31
+ 4. They add provider buttons to their existing login page
32
+ 5. They include the authentication concern in their User model
33
+ 6. They add provider credentials via environment variables or Rails credentials
34
+ 7. It just works
35
+
36
+ ## Implementation Phases
37
+
38
+ ### Phase 1: Core Infrastructure (2 weeks)
39
+
40
+ 1. **Set up gem structure**
41
+ - Define module structure and namespaces
42
+ - Set up configuration options and defaults
43
+ - Add Faraday dependency for HTTP communications
44
+
45
+ 2. **Implement OIDC/OAuth2 Core**
46
+ - Authorization Code Flow implementation
47
+ - Token handling and validation
48
+ - ID token processing
49
+ - State management for security
50
+
51
+ 3. **Provider Implementations**
52
+ - Abstract provider class
53
+ - Implementation for Google (reference implementation)
54
+ - Standardized approach for provider-specific quirks
55
+ - Configuration validation logic
56
+
57
+ ### Phase 2: Rails Integration (2 weeks)
58
+
59
+ 1. **Rails Engine Setup**
60
+ - Mount routes
61
+ - Controller templates
62
+ - Configuration integration
63
+
64
+ 2. **Generators**
65
+ - Idempotent controller generator
66
+ - Idempotent views generator
67
+ - Idempotent configuration generator
68
+ - Migration generator for user model
69
+
70
+ 3. **View Helpers**
71
+ - Individual button helpers with provider validation
72
+ - Styling helpers with customization options
73
+
74
+ ### Phase 3: Additional Providers & Testing (2 weeks)
75
+
76
+ 1. **Additional Provider Implementations**
77
+ - GitHub
78
+ - Apple
79
+ - Facebook
80
+ - Microsoft
81
+
82
+ 2. **Testing Infrastructure**
83
+ - Unit tests for core components
84
+ - Integration tests for flows
85
+ - Stubbed provider responses
86
+
87
+ 3. **Documentation**
88
+ - Usage examples
89
+ - Configuration options
90
+ - Customization guide
91
+
92
+ ### Phase 4: Refinement & QA (1 week)
93
+
94
+ 1. **Security Review**
95
+ - Validate token handling
96
+ - Verify state management
97
+ - Ensure proper error handling
98
+
99
+ 2. **Performance Optimization**
100
+ - Response caching where appropriate
101
+ - Minimize unnecessary requests
102
+
103
+ 3. **Final Polish**
104
+ - Ensure consistent styling
105
+ - Complete documentation
106
+ - Version 1.0.0 preparation
107
+
108
+ ## Technical Architecture
109
+
110
+ ### Core Components
111
+
112
+ 1. **Configuration**
113
+ ```ruby
114
+ # lib/clavis/configuration.rb
115
+ module Clavis
116
+ class Configuration
117
+ attr_accessor :providers, :default_callback_path, :default_scopes
118
+
119
+ def provider_configured?(provider_name)
120
+ providers&.key?(provider_name.to_sym) &&
121
+ providers[provider_name.to_sym][:client_id].present? &&
122
+ providers[provider_name.to_sym][:client_secret].present?
123
+ end
124
+
125
+ def validate_provider!(provider_name)
126
+ raise Clavis::ProviderNotConfigured.new(provider_name) unless provider_configured?(provider_name)
127
+ end
128
+ end
129
+ end
130
+ ```
131
+
132
+ 2. **Provider Base Class**
133
+ ```ruby
134
+ # lib/clavis/providers/base.rb
135
+ module Clavis
136
+ module Providers
137
+ class Base
138
+ attr_reader :client_id, :client_secret, :redirect_uri
139
+
140
+ def initialize(config = {})
141
+ @client_id = config[:client_id] ||
142
+ ENV["CLAVIS_#{provider_name.upcase}_CLIENT_ID"] ||
143
+ Rails.application.credentials.dig(:clavis, provider_name, :client_id)
144
+
145
+ @client_secret = config[:client_secret] ||
146
+ ENV["CLAVIS_#{provider_name.upcase}_CLIENT_SECRET"] ||
147
+ Rails.application.credentials.dig(:clavis, provider_name, :client_secret)
148
+
149
+ @redirect_uri = config[:redirect_uri]
150
+
151
+ validate_configuration!
152
+ end
153
+
154
+ def authorize_url(state:, nonce:, scope:)
155
+ # Build authorization URL
156
+ end
157
+
158
+ def token_exchange(code:)
159
+ # Exchange code for tokens using Faraday
160
+ end
161
+
162
+ def parse_id_token(token)
163
+ # Parse and validate the ID token
164
+ end
165
+
166
+ private
167
+
168
+ def validate_configuration!
169
+ raise Clavis::MissingConfiguration.new("client_id for #{provider_name}") if @client_id.blank?
170
+ raise Clavis::MissingConfiguration.new("client_secret for #{provider_name}") if @client_secret.blank?
171
+ end
172
+ end
173
+ end
174
+ end
175
+ ```
176
+
177
+ 3. **Authentication Controller Concern**
178
+ ```ruby
179
+ # lib/clavis/controllers/concerns/authentication.rb
180
+ module Clavis
181
+ module Controllers
182
+ module Authentication
183
+ extend ActiveSupport::Concern
184
+
185
+ def oauth_authorize
186
+ provider = Clavis.provider(params[:provider])
187
+ redirect_to provider.authorize_url(
188
+ state: generate_state,
189
+ nonce: generate_nonce,
190
+ scope: params[:scope] || Clavis.configuration.default_scopes
191
+ )
192
+ end
193
+
194
+ def oauth_callback
195
+ provider = Clavis.provider(params[:provider])
196
+ auth_hash = provider.process_callback(params[:code], session.delete(:oauth_state))
197
+ user = find_or_create_user_from_oauth(auth_hash)
198
+
199
+ # Let the application handle the user authentication
200
+ yield(user, auth_hash) if block_given?
201
+ end
202
+
203
+ private
204
+
205
+ def generate_state
206
+ state = SecureRandom.hex(24)
207
+ session[:oauth_state] = state
208
+ state
209
+ end
210
+
211
+ def generate_nonce
212
+ SecureRandom.hex(16)
213
+ end
214
+ end
215
+ end
216
+ end
217
+ ```
218
+
219
+ 4. **User Authentication Concern**
220
+ ```ruby
221
+ # lib/clavis/models/concerns/oauth_authenticatable.rb
222
+ module Clavis
223
+ module Models
224
+ module OauthAuthenticatable
225
+ extend ActiveSupport::Concern
226
+
227
+ class_methods do
228
+ def find_for_oauth(auth_hash)
229
+ user = find_by(provider: auth_hash[:provider], uid: auth_hash[:uid])
230
+
231
+ unless user
232
+ user = new(
233
+ provider: auth_hash[:provider],
234
+ uid: auth_hash[:uid],
235
+ email: auth_hash[:info][:email],
236
+ # Additional fields as needed
237
+ )
238
+
239
+ # Allow customization via a block
240
+ yield(user, auth_hash) if block_given?
241
+
242
+ user.save!
243
+ end
244
+
245
+ user
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+ ```
252
+
253
+ 5. **View Helpers**
254
+ ```ruby
255
+ # lib/clavis/view_helpers.rb
256
+ module Clavis
257
+ module ViewHelpers
258
+ def clavis_oauth_button(provider, options = {})
259
+ # Validate provider configuration
260
+ Clavis.configuration.validate_provider!(provider)
261
+
262
+ # Render button with proper styling and SVG
263
+ button_text = options.delete(:text) || "Sign in with #{provider.to_s.titleize}"
264
+ button_class = "clavis-button clavis-#{provider}-button #{options.delete(:class)}"
265
+
266
+ link_to auth_authorize_path(provider), class: button_class, method: :post, data: options[:data] do
267
+ provider_svg(provider) + content_tag(:span, button_text)
268
+ end
269
+ rescue Clavis::ProviderNotConfigured => e
270
+ # Return error message or comment in development/test
271
+ if Rails.env.development? || Rails.env.test?
272
+ content_tag(:div, "#{provider} not configured. Add client_id and client_secret.", class: 'clavis-error')
273
+ else
274
+ Rails.logger.error("Attempted to use unconfigured provider: #{provider}")
275
+ nil
276
+ end
277
+ end
278
+
279
+ def provider_svg(provider)
280
+ # Return SVG for the provider
281
+ end
282
+ end
283
+ end
284
+ ```
285
+
286
+ ### Generators
287
+
288
+ 1. **Install Generator**
289
+ ```ruby
290
+ # lib/generators/clavis/install_generator.rb
291
+ module Clavis
292
+ class InstallGenerator < Rails::Generators::Base
293
+ source_root File.expand_path("../templates", __FILE__)
294
+
295
+ class_option :providers, type: :array, default: []
296
+
297
+ def create_initializer
298
+ template "initializer.rb", "config/initializers/clavis.rb"
299
+ end
300
+
301
+ def create_migration
302
+ migration_template "migration.rb", "db/migrate/add_oauth_to_users.rb", skip: true
303
+ end
304
+
305
+ def mount_engine
306
+ route "mount Clavis::Engine => '/auth'"
307
+ end
308
+
309
+ def create_controllers
310
+ generate "clavis:controllers", options[:providers].join(" ")
311
+ end
312
+
313
+ def create_views
314
+ generate "clavis:views", options[:providers].join(" ")
315
+ end
316
+
317
+ def add_user_concern
318
+ inject_into_file "app/models/user.rb", after: "class User < ApplicationRecord\n" do
319
+ " include Clavis::Models::OauthAuthenticatable\n"
320
+ end
321
+ end
322
+
323
+ def show_post_install_message
324
+ say "\nClavis has been installed! Next steps:"
325
+ say "1. Run migrations: rails db:migrate"
326
+ say "2. Configure your providers in config/initializers/clavis.rb"
327
+ say "3. Add provider buttons to your views: <%= clavis_oauth_button :google %>"
328
+ say "\nFor more information, see the documentation at https://github.com/clayton/clavis"
329
+ end
330
+ end
331
+ end
332
+ ```
333
+
334
+ 2. **Controllers Generator**
335
+ ```ruby
336
+ # lib/generators/clavis/controllers_generator.rb
337
+ module Clavis
338
+ class ControllersGenerator < Rails::Generators::Base
339
+ source_root File.expand_path("../templates", __FILE__)
340
+
341
+ argument :providers, type: :array, default: []
342
+
343
+ def create_controllers
344
+ template "auth_controller.rb", "app/controllers/clavis/auth_controller.rb"
345
+ end
346
+
347
+ def create_example_controller
348
+ template "sessions_controller.rb", "app/controllers/clavis/sessions_controller.rb"
349
+ end
350
+ end
351
+ end
352
+ ```
353
+
354
+ 3. **Views Generator**
355
+ ```ruby
356
+ # lib/generators/clavis/views_generator.rb
357
+ module Clavis
358
+ class ViewsGenerator < Rails::Generators::Base
359
+ source_root File.expand_path("../templates", __FILE__)
360
+
361
+ argument :providers, type: :array, default: []
362
+
363
+ def create_views
364
+ directory "views", "app/views/clavis"
365
+ end
366
+
367
+ def create_provider_specific_views
368
+ providers.each do |provider|
369
+ @provider = provider
370
+ template "views/providers/_button.html.erb", "app/views/clavis/providers/_#{provider}_button.html.erb"
371
+ end
372
+ end
373
+ end
374
+ end
375
+ ```
376
+
377
+ ### Required Migrations
378
+
379
+ ```ruby
380
+ # lib/generators/clavis/templates/migration.rb
381
+ class AddOauthToUsers < ActiveRecord::Migration[8.0]
382
+ def change
383
+ add_column :users, :provider, :string unless column_exists?(:users, :provider)
384
+ add_column :users, :uid, :string unless column_exists?(:users, :uid)
385
+ add_column :users, :oauth_token, :string unless column_exists?(:users, :oauth_token)
386
+ add_column :users, :oauth_expires_at, :datetime unless column_exists?(:users, :oauth_expires_at)
387
+
388
+ add_index :users, [:provider, :uid], unique: true unless index_exists?(:users, [:provider, :uid])
389
+ end
390
+ end
391
+ ```
392
+
393
+ ## Dependencies
394
+
395
+ - **Faraday**: HTTP client library for API requests
396
+ - **JWT**: JSON Web Token implementation for token validation
397
+ - **Rails (>= 8.0)**: Framework integration
398
+
399
+ ## Testing Strategy
400
+
401
+ 1. **Unit Tests**
402
+ - Test core components in isolation
403
+ - Mock HTTP responses for predictability
404
+ - Validate token generation/parsing
405
+
406
+ 2. **Integration Tests**
407
+ - Test full authentication flows
408
+ - Stub provider responses
409
+ - Verify session management
410
+
411
+ 3. **Security Tests**
412
+ - Verify CSRF protection
413
+ - Test state parameter validation
414
+ - Ensure proper error handling for invalid tokens
415
+
416
+ ## Example Usage
417
+
418
+ ### Configuration
419
+
420
+ ```ruby
421
+ # config/initializers/clavis.rb
422
+ Clavis.configure do |config|
423
+ config.providers = {
424
+ google: {
425
+ client_id: ENV['GOOGLE_CLIENT_ID'],
426
+ client_secret: ENV['GOOGLE_CLIENT_SECRET'],
427
+ redirect_uri: 'https://myapp.com/auth/google/callback'
428
+ },
429
+ github: {
430
+ client_id: Rails.application.credentials.dig(:github, :client_id),
431
+ client_secret: Rails.application.credentials.dig(:github, :client_secret),
432
+ redirect_uri: 'https://myapp.com/auth/github/callback'
433
+ }
434
+ }
435
+ end
436
+ ```
437
+
438
+ ### User Model
439
+
440
+ ```ruby
441
+ # app/models/user.rb
442
+ class User < ApplicationRecord
443
+ include Clavis::Models::OauthAuthenticatable
444
+
445
+ # Customize user creation if needed
446
+ def self.find_for_oauth(auth_hash)
447
+ super do |user, auth|
448
+ user.name = auth[:info][:name]
449
+ user.avatar_url = auth[:info][:image]
450
+ end
451
+ end
452
+ end
453
+ ```
454
+
455
+ ### Controller
456
+
457
+ ```ruby
458
+ # app/controllers/sessions_controller.rb
459
+ class SessionsController < ApplicationController
460
+ include Clavis::Controllers::Authentication
461
+
462
+ def create_from_oauth
463
+ oauth_callback do |user, auth_hash|
464
+ # Sign the user in
465
+ session[:user_id] = user.id
466
+ redirect_to root_path, notice: "Signed in with #{auth_hash[:provider].to_s.titleize}!"
467
+ end
468
+ end
469
+ end
470
+ ```
471
+
472
+ ### View
473
+
474
+ ```erb
475
+ <%# app/views/sessions/new.html.erb %>
476
+ <h1>Sign in</h1>
477
+
478
+ <%= form_with url: login_path, method: :post do |f| %>
479
+ <div class="field">
480
+ <%= f.label :email %>
481
+ <%= f.email_field :email %>
482
+ </div>
483
+
484
+ <div class="field">
485
+ <%= f.label :password %>
486
+ <%= f.password_field :password %>
487
+ </div>
488
+
489
+ <div class="actions">
490
+ <%= f.submit "Sign in" %>
491
+ </div>
492
+ <% end %>
493
+
494
+ <div class="oauth-providers">
495
+ <p>Or sign in with:</p>
496
+ <%= clavis_oauth_button :google %>
497
+ <%= clavis_oauth_button :github %>
498
+ <%= clavis_oauth_button :apple %>
499
+ </div>
500
+ ```
501
+
502
+ ## Risk Assessment
503
+
504
+ 1. **Provider API Changes**
505
+ - Mitigation: Version-specific implementations with fallbacks
506
+ - Regular testing against live endpoints
507
+
508
+ 2. **Security Vulnerabilities**
509
+ - Mitigation: Thorough security review
510
+ - Follow OIDC/OAuth2 best practices strictly
511
+ - Regular updates for security patches
512
+
513
+ 3. **Rails Version Compatibility**
514
+ - Mitigation: Clear version requirements
515
+ - Testing against multiple Rails versions
516
+
517
+ ## Future Enhancements (Post 1.0)
518
+
519
+ 1. Account linking (multiple providers per user)
520
+ 2. JWT support as an alternative to session-based authentication
521
+ 3. Additional providers (Twitter, LinkedIn, etc.)
522
+ 4. Enhanced customization options
523
+ 5. Internationalization support
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clavis
4
+ class Configuration
5
+ SUPPORTED_PROVIDERS = %i[google github facebook apple microsoft].freeze
6
+
7
+ attr_accessor :providers, :default_callback_path, :default_scopes, :verbose_logging, :claims_processor,
8
+ :encrypt_tokens, :encryption_key, :use_rails_credentials, :parameter_filter_enabled,
9
+ :allowed_redirect_hosts, :exact_redirect_uri_matching, :allow_localhost_in_development,
10
+ :raise_on_invalid_redirect, :enforce_https, :allow_http_localhost, :verify_ssl,
11
+ :minimum_tls_version, :validate_inputs, :sanitize_inputs, :rotate_session_after_login,
12
+ :session_key_prefix, :logger, :log_level, :token_encryption_key, :csrf_protection_enabled,
13
+ :valid_redirect_schemes, :view_helpers_auto_include, :user_class, :user_finder_method,
14
+ :rate_limiting_enabled, :custom_throttles
15
+
16
+ def initialize
17
+ @providers = {}
18
+ @default_callback_path = "/auth/:provider/callback"
19
+ @default_scopes = nil
20
+ @verbose_logging = false
21
+ @claims_processor = nil
22
+
23
+ # Security-related defaults
24
+ @encrypt_tokens = false
25
+ @encryption_key = nil
26
+ @use_rails_credentials = defined?(Rails)
27
+ @parameter_filter_enabled = true
28
+
29
+ # Redirect URI validation defaults
30
+ @allowed_redirect_hosts = []
31
+ @exact_redirect_uri_matching = false
32
+ @allow_localhost_in_development = true
33
+ @raise_on_invalid_redirect = true
34
+
35
+ # HTTPS enforcement defaults
36
+ @enforce_https = true
37
+ @allow_http_localhost = true
38
+ @verify_ssl = true
39
+ @minimum_tls_version = :TLS1_2
40
+
41
+ # Input validation configuration
42
+ @validate_inputs = true
43
+ @sanitize_inputs = true
44
+
45
+ # Session management configuration
46
+ @rotate_session_after_login = true
47
+ @session_key_prefix = "clavis"
48
+
49
+ # Additional configuration options
50
+ @logger = nil
51
+ @log_level = :info
52
+ @token_encryption_key = nil
53
+ @csrf_protection_enabled = true
54
+ @valid_redirect_schemes = %w[http https]
55
+ @view_helpers_auto_include = true
56
+
57
+ # User creation configuration
58
+ @user_class = "User"
59
+ @user_finder_method = :find_or_create_from_clavis
60
+
61
+ # Rate limiting configuration
62
+ @rate_limiting_enabled = true
63
+ @custom_throttles = {}
64
+ end
65
+
66
+ # Returns the list of supported providers
67
+ # @return [Array<Symbol>] List of supported provider symbols
68
+ def self.supported_providers
69
+ SUPPORTED_PROVIDERS
70
+ end
71
+
72
+ # Returns the list of configured providers
73
+ # @return [Array<Symbol>] List of configured provider symbols
74
+ def configured_providers
75
+ providers.keys
76
+ end
77
+
78
+ def post_initialize
79
+ # Set up engine view helpers based on configuration
80
+ Clavis::Engine.include_view_helpers = @view_helpers_auto_include if defined?(Clavis::Engine)
81
+ end
82
+
83
+ def provider_configured?(provider_name)
84
+ provider_sym = provider_name.to_sym
85
+
86
+ # Check if the provider is defined in the configuration
87
+ return false unless providers&.key?(provider_sym)
88
+
89
+ provider_config = providers[provider_sym]
90
+
91
+ # Handle empty provider config or non-hash values
92
+ return false unless provider_config.is_a?(Hash)
93
+
94
+ # Check for required credentials
95
+ if provider_config[:client_id].nil? || provider_config[:client_id].to_s.strip.empty?
96
+ Clavis::Logging.log_error("Provider '#{provider_name}' is missing client_id")
97
+ return false
98
+ end
99
+
100
+ if provider_config[:client_secret].nil? || provider_config[:client_secret].to_s.strip.empty?
101
+ Clavis::Logging.log_error("Provider '#{provider_name}' is missing client_secret")
102
+ return false
103
+ end
104
+
105
+ # Apple doesn't always require a redirect_uri
106
+ if provider_sym != :apple &&
107
+ (provider_config[:redirect_uri].nil? ||
108
+ provider_config[:redirect_uri].to_s.strip.empty?)
109
+ Clavis::Logging.log_error("Provider '#{provider_name}' is missing redirect_uri")
110
+ return false
111
+ end
112
+
113
+ # All checks passed
114
+ true
115
+ end
116
+
117
+ def validate_provider!(provider_name)
118
+ provider_sym = provider_name.to_sym
119
+
120
+ # Check if the provider is defined in the configuration
121
+ unless providers&.key?(provider_sym)
122
+ raise Clavis::ProviderNotConfigured, "Provider '#{provider_name}' is not defined in the configuration"
123
+ end
124
+
125
+ provider_config = providers[provider_sym]
126
+
127
+ # Handle empty provider config or non-hash values
128
+ unless provider_config.is_a?(Hash)
129
+ raise Clavis::ProviderNotConfigured, "Provider '#{provider_name}' has invalid configuration"
130
+ end
131
+
132
+ # Check for required credentials
133
+ if provider_config[:client_id].nil? || provider_config[:client_id].to_s.strip.empty?
134
+ raise Clavis::ProviderNotConfigured, "Provider '#{provider_name}' is missing client_id"
135
+ end
136
+
137
+ if provider_config[:client_secret].nil? || provider_config[:client_secret].to_s.strip.empty?
138
+ raise Clavis::ProviderNotConfigured, "Provider '#{provider_name}' is missing client_secret"
139
+ end
140
+
141
+ # Apple doesn't always require a redirect_uri
142
+ return if provider_sym == :apple
143
+ return unless provider_config[:redirect_uri].nil? || provider_config[:redirect_uri].to_s.strip.empty?
144
+
145
+ raise Clavis::ProviderNotConfigured, "Provider '#{provider_name}' is missing redirect_uri"
146
+ end
147
+
148
+ def provider_config(provider_name)
149
+ validate_provider!(provider_name)
150
+
151
+ # If Rails credentials are enabled, merge with provider config
152
+ if use_rails_credentials && defined?(Rails) && Rails.application.respond_to?(:credentials)
153
+ credentials_config = Rails.application.credentials.dig(:clavis, :providers, provider_name.to_sym)
154
+ if credentials_config && !credentials_config.to_h.empty?
155
+ return providers[provider_name.to_sym].merge(credentials_config.to_h)
156
+ end
157
+ end
158
+
159
+ providers[provider_name.to_sym]
160
+ end
161
+
162
+ def callback_path(provider_name)
163
+ provider_name = provider_name.to_sym if provider_name.is_a?(String)
164
+
165
+ # Ensure the provider is configured properly first
166
+ provider_cfg = provider_config(provider_name)
167
+
168
+ # Use provider's redirect_uri if specified, otherwise use default
169
+ path = provider_cfg[:redirect_uri] || default_callback_path
170
+
171
+ # Replace :provider placeholder with the actual provider name
172
+ path = path.gsub(":provider", provider_name.to_s) if path.include?(":provider")
173
+
174
+ path
175
+ end
176
+
177
+ def effective_encryption_key
178
+ return nil unless encrypt_tokens
179
+
180
+ if use_rails_credentials && defined?(Rails) && Rails.application.respond_to?(:credentials)
181
+ rails_key = Rails.application.credentials.dig(:clavis, :encryption_key)
182
+ return rails_key if rails_key && !rails_key.to_s.empty?
183
+ end
184
+
185
+ encryption_key
186
+ end
187
+
188
+ def should_verify_ssl?
189
+ # Always verify SSL in production
190
+ return true if defined?(Rails) && Rails.env.production?
191
+
192
+ # Otherwise, use the configured value
193
+ verify_ssl
194
+ end
195
+ end
196
+ end