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.
- checksums.yaml +7 -0
- data/.actrc +4 -0
- data/.cursor/rules/ruby-gem.mdc +49 -0
- data/.gemignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.vscode/settings.json +22 -0
- data/CHANGELOG.md +127 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +838 -0
- data/Rakefile +341 -0
- data/UPGRADE.md +57 -0
- data/app/assets/stylesheets/clavis.css +133 -0
- data/app/controllers/clavis/auth_controller.rb +133 -0
- data/config/database.yml +16 -0
- data/config/routes.rb +49 -0
- data/docs/SECURITY.md +340 -0
- data/docs/TESTING.md +78 -0
- data/docs/integration.md +272 -0
- data/error_handling.md +355 -0
- data/file_structure.md +221 -0
- data/gemfiles/rails_80.gemfile +17 -0
- data/gemfiles/rails_80.gemfile.lock +286 -0
- data/implementation_plan.md +523 -0
- data/lib/clavis/configuration.rb +196 -0
- data/lib/clavis/controllers/concerns/authentication.rb +232 -0
- data/lib/clavis/controllers/concerns/session_management.rb +117 -0
- data/lib/clavis/engine.rb +191 -0
- data/lib/clavis/errors.rb +205 -0
- data/lib/clavis/logging.rb +116 -0
- data/lib/clavis/models/concerns/oauth_authenticatable.rb +169 -0
- data/lib/clavis/oauth_identity.rb +174 -0
- data/lib/clavis/providers/apple.rb +135 -0
- data/lib/clavis/providers/base.rb +432 -0
- data/lib/clavis/providers/custom_provider_example.rb +57 -0
- data/lib/clavis/providers/facebook.rb +84 -0
- data/lib/clavis/providers/generic.rb +63 -0
- data/lib/clavis/providers/github.rb +87 -0
- data/lib/clavis/providers/google.rb +98 -0
- data/lib/clavis/providers/microsoft.rb +57 -0
- data/lib/clavis/security/csrf_protection.rb +79 -0
- data/lib/clavis/security/https_enforcer.rb +90 -0
- data/lib/clavis/security/input_validator.rb +192 -0
- data/lib/clavis/security/parameter_filter.rb +64 -0
- data/lib/clavis/security/rate_limiter.rb +109 -0
- data/lib/clavis/security/redirect_uri_validator.rb +124 -0
- data/lib/clavis/security/session_manager.rb +220 -0
- data/lib/clavis/security/token_storage.rb +114 -0
- data/lib/clavis/user_info_normalizer.rb +74 -0
- data/lib/clavis/utils/nonce_store.rb +14 -0
- data/lib/clavis/utils/secure_token.rb +17 -0
- data/lib/clavis/utils/state_store.rb +18 -0
- data/lib/clavis/version.rb +6 -0
- data/lib/clavis/view_helpers.rb +260 -0
- data/lib/clavis.rb +132 -0
- data/lib/generators/clavis/controller/controller_generator.rb +48 -0
- data/lib/generators/clavis/controller/templates/controller.rb.tt +137 -0
- data/lib/generators/clavis/controller/templates/views/login.html.erb.tt +145 -0
- data/lib/generators/clavis/install_generator.rb +182 -0
- data/lib/generators/clavis/templates/add_oauth_to_users.rb +28 -0
- data/lib/generators/clavis/templates/clavis.css +133 -0
- data/lib/generators/clavis/templates/initializer.rb +47 -0
- data/lib/generators/clavis/templates/initializer.rb.tt +76 -0
- data/lib/generators/clavis/templates/migration.rb +18 -0
- data/lib/generators/clavis/templates/migration.rb.tt +16 -0
- data/lib/generators/clavis/user_method/user_method_generator.rb +219 -0
- data/lib/tasks/provider_verification.rake +77 -0
- data/llms.md +487 -0
- data/log/development.log +20 -0
- data/log/test.log +0 -0
- data/sig/clavis.rbs +4 -0
- data/testing_plan.md +710 -0
- metadata +258 -0
data/Rakefile
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
task default: :ci
|
9
|
+
|
10
|
+
# Helper method to safely run a Rails command and fix known issues
|
11
|
+
def safe_rails_command(rails_app_dir, command)
|
12
|
+
Dir.chdir(rails_app_dir) do
|
13
|
+
# Fix bootsnap issue if needed
|
14
|
+
fix_bootsnap_issue
|
15
|
+
|
16
|
+
# Try running the command
|
17
|
+
puts "Running: #{command}"
|
18
|
+
result = system(command)
|
19
|
+
|
20
|
+
# If it fails, check for asset configuration issues
|
21
|
+
unless result
|
22
|
+
puts "Command failed, checking for asset configuration issues..."
|
23
|
+
|
24
|
+
# Try to fix assets.rb if it exists
|
25
|
+
assets_initializer = "config/initializers/assets.rb"
|
26
|
+
if File.exist?(assets_initializer)
|
27
|
+
puts "Fixing assets configuration..."
|
28
|
+
# Comment out the entire file to prevent issues
|
29
|
+
content = File.read(assets_initializer)
|
30
|
+
fixed_content = "# Assets configuration disabled for testing\n# Original content:\n# #{content.gsub("\n",
|
31
|
+
"\n# ")}"
|
32
|
+
File.write(assets_initializer, fixed_content)
|
33
|
+
|
34
|
+
# Try the command again
|
35
|
+
puts "Retrying: #{command}"
|
36
|
+
result = system(command)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
return result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Helper to fix bootsnap issues and other dependencies in the Rails app
|
45
|
+
def fix_bootsnap_issue
|
46
|
+
boot_rb_path = "config/boot.rb"
|
47
|
+
return unless File.exist?(boot_rb_path)
|
48
|
+
|
49
|
+
content = File.read(boot_rb_path)
|
50
|
+
return unless content.include?("bootsnap/setup") && !system("bundle list | grep bootsnap")
|
51
|
+
|
52
|
+
puts "Fixing bootsnap issue..."
|
53
|
+
|
54
|
+
# Option 1: Add bootsnap to Gemfile
|
55
|
+
unless File.read("Gemfile").include?("bootsnap")
|
56
|
+
puts "Adding bootsnap to Gemfile..."
|
57
|
+
File.open("Gemfile", "a") do |f|
|
58
|
+
f.puts "\n# Reduces boot times through caching; required in config/boot.rb"
|
59
|
+
f.puts "gem \"bootsnap\", require: false"
|
60
|
+
end
|
61
|
+
system("bundle install")
|
62
|
+
end
|
63
|
+
|
64
|
+
# Option 2 (fallback): Comment out bootsnap line in boot.rb
|
65
|
+
return if system("bundle list | grep bootsnap")
|
66
|
+
|
67
|
+
puts "Commenting out bootsnap in boot.rb..."
|
68
|
+
modified_content = content.gsub(
|
69
|
+
'require "bootsnap/setup"',
|
70
|
+
'# require "bootsnap/setup" # Commented out to avoid dependency issues'
|
71
|
+
)
|
72
|
+
File.write(boot_rb_path, modified_content)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Helper to test loading the clavis gem
|
76
|
+
def test_clavis_loading(rails_app_dir)
|
77
|
+
Dir.chdir(rails_app_dir) do
|
78
|
+
puts "Testing if the clavis gem can be loaded..."
|
79
|
+
test_code = "require 'clavis'; puts 'Clavis loaded successfully! Version: ' + Clavis::VERSION"
|
80
|
+
system("bundle exec ruby -e \"#{test_code}\"")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Define an environment task for Rails-dependent tasks
|
85
|
+
task :environment do
|
86
|
+
# This is a no-op task to satisfy dependencies
|
87
|
+
# Rails would normally provide this task
|
88
|
+
end
|
89
|
+
|
90
|
+
# Task to run Rails-dependent tests
|
91
|
+
# rubocop:disable Metrics/BlockLength
|
92
|
+
namespace :test do
|
93
|
+
desc "Run Rails controller tests"
|
94
|
+
task controllers: :environment do
|
95
|
+
ENV["RAILS_ENV"] = "test"
|
96
|
+
system("bundle exec rspec spec/clavis/controllers/*_spec.rb")
|
97
|
+
end
|
98
|
+
|
99
|
+
desc "Run Rails integration tests"
|
100
|
+
task integration: :environment do
|
101
|
+
ENV["RAILS_ENV"] = "test"
|
102
|
+
system("bundle exec rspec spec/integration/*_spec.rb")
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Run Rails generator tests"
|
106
|
+
task generators: :environment do
|
107
|
+
ENV["RAILS_ENV"] = "test"
|
108
|
+
system("bundle exec rspec spec/generators/**/*_generator_spec.rb")
|
109
|
+
end
|
110
|
+
|
111
|
+
desc "Run all Rails-dependent tests"
|
112
|
+
task rails: %i[controllers integration generators]
|
113
|
+
|
114
|
+
desc "Test the actual generator in the rails-app directory"
|
115
|
+
task real_generator: :bootstrap_rails_app do
|
116
|
+
puts "Testing basic functionality in rails-app..."
|
117
|
+
rails_app_dir = File.expand_path("rails-app", __dir__)
|
118
|
+
|
119
|
+
# The bootstrap task ensures the directory exists, so we can remove this check
|
120
|
+
# or keep it for extra safety
|
121
|
+
unless File.directory?(rails_app_dir)
|
122
|
+
puts "Error: rails-app directory not found"
|
123
|
+
exit 1
|
124
|
+
end
|
125
|
+
|
126
|
+
Dir.chdir(rails_app_dir) do
|
127
|
+
puts "Adding bcrypt"
|
128
|
+
unless system("bundle add bcrypt")
|
129
|
+
puts "Error: Failed to add bcrypt"
|
130
|
+
exit 1
|
131
|
+
end
|
132
|
+
|
133
|
+
puts "Adding clavis"
|
134
|
+
unless system("bundle add clavis")
|
135
|
+
puts "Error: Failed to add clavis"
|
136
|
+
exit 1
|
137
|
+
end
|
138
|
+
|
139
|
+
# Install dependencies
|
140
|
+
puts "Installing dependencies in rails-app..."
|
141
|
+
unless system("bundle install")
|
142
|
+
puts "Error: Failed to install dependencies in rails-app"
|
143
|
+
exit 1
|
144
|
+
end
|
145
|
+
|
146
|
+
# Test loading the gem
|
147
|
+
unless test_clavis_loading(rails_app_dir)
|
148
|
+
puts "Error: Failed to load the clavis gem in rails-app"
|
149
|
+
exit 1
|
150
|
+
end
|
151
|
+
|
152
|
+
# Run our clavis generator
|
153
|
+
puts "Running Clavis generator..."
|
154
|
+
unless safe_rails_command(rails_app_dir, "bin/rails generate clavis:install")
|
155
|
+
puts "Error: Failed to run Clavis generator"
|
156
|
+
exit 1
|
157
|
+
end
|
158
|
+
|
159
|
+
# Run migrations again after the generator
|
160
|
+
puts "Running migrations after generator..."
|
161
|
+
unless safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=development") &&
|
162
|
+
safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=test")
|
163
|
+
puts "Error: Failed to run migrations after generator"
|
164
|
+
exit 1
|
165
|
+
end
|
166
|
+
|
167
|
+
puts "Checking routes"
|
168
|
+
unless safe_rails_command(rails_app_dir, "bin/rails routes")
|
169
|
+
puts "Error: Failed to check routes"
|
170
|
+
exit 1
|
171
|
+
end
|
172
|
+
|
173
|
+
puts "Generator dependency test in rails-app passed successfully!"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
# Task to run all tests
|
178
|
+
desc "Run all tests including Rails controller tests and integration tests"
|
179
|
+
task all_tests: [:spec, "test:rails", "test:real_generator"]
|
180
|
+
|
181
|
+
# Helper to set up rails app authentication
|
182
|
+
def setup_rails_authentication(rails_app_dir, gemfile_content)
|
183
|
+
Dir.chdir(rails_app_dir) do
|
184
|
+
# Add bcrypt first to ensure it's available for has_secure_password
|
185
|
+
unless gemfile_content.include?("gem \"bcrypt\"")
|
186
|
+
puts "Adding bcrypt to Gemfile..."
|
187
|
+
File.open("Gemfile", "a") do |f|
|
188
|
+
f.puts "\n# Use Active Model has_secure_password"
|
189
|
+
f.puts "gem \"bcrypt\", \"~> 3.1.7\""
|
190
|
+
end
|
191
|
+
puts "Installing bcrypt..."
|
192
|
+
system("bundle install")
|
193
|
+
end
|
194
|
+
|
195
|
+
# Use the Rails 8 built-in authentication generator
|
196
|
+
puts "Using Rails built-in authentication generator..."
|
197
|
+
system("bin/rails generate authentication")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Task to bootstrap rails-app if it doesn't exist
|
202
|
+
desc "Create a minimal Rails application for testing if rails-app doesn't exist"
|
203
|
+
task :bootstrap_rails_app do
|
204
|
+
rails_app_dir = File.expand_path("rails-app", __dir__)
|
205
|
+
|
206
|
+
if File.directory?(rails_app_dir)
|
207
|
+
puts "Rails application already exists at #{rails_app_dir}"
|
208
|
+
|
209
|
+
# Even if it exists, make sure the database is migrated
|
210
|
+
puts "Ensuring database is migrated..."
|
211
|
+
safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=development")
|
212
|
+
safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=test")
|
213
|
+
else
|
214
|
+
puts "Creating Rails application at #{rails_app_dir}..."
|
215
|
+
|
216
|
+
# Check if Rails is installed
|
217
|
+
unless system("gem list rails -i")
|
218
|
+
puts "Installing Rails..."
|
219
|
+
system("gem install rails")
|
220
|
+
end
|
221
|
+
|
222
|
+
# Create a new Rails application with minimal sensible flags
|
223
|
+
system("rails new #{rails_app_dir} --skip-git")
|
224
|
+
|
225
|
+
# Add the clavis gem to the Gemfile
|
226
|
+
gemfile_path = File.join(rails_app_dir, "Gemfile")
|
227
|
+
|
228
|
+
# Read the current Gemfile
|
229
|
+
gemfile_content = File.read(gemfile_path)
|
230
|
+
|
231
|
+
# Add clavis with path to local directory
|
232
|
+
unless gemfile_content.include?("gem \"clavis\"")
|
233
|
+
puts "Adding clavis gem to Gemfile..."
|
234
|
+
gem_line = "gem \"clavis\", path: \"../.\""
|
235
|
+
|
236
|
+
# Append to Gemfile
|
237
|
+
File.open(gemfile_path, "a") do |f|
|
238
|
+
f.puts "\n# Add local Clavis gem for testing"
|
239
|
+
f.puts gem_line
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
Dir.chdir(rails_app_dir) do
|
244
|
+
puts "Installing dependencies..."
|
245
|
+
system("bundle install")
|
246
|
+
|
247
|
+
# Fix bootsnap issue
|
248
|
+
fix_bootsnap_issue
|
249
|
+
|
250
|
+
# Fix asset configuration if needed
|
251
|
+
assets_initializer = "config/initializers/assets.rb"
|
252
|
+
if File.exist?(assets_initializer)
|
253
|
+
puts "Checking assets configuration..."
|
254
|
+
assets_content = File.read(assets_initializer)
|
255
|
+
if assets_content.include?("Rails.application.config.assets") &&
|
256
|
+
!system("bin/rails runner 'Rails.application.config.respond_to?(:assets)'")
|
257
|
+
puts "Fixing assets configuration..."
|
258
|
+
fixed_content = assets_content.gsub(/Rails\.application\.config\.assets.*$/,
|
259
|
+
"# Assets configuration disabled for testing")
|
260
|
+
File.write(assets_initializer, fixed_content)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
safe_rails_command(rails_app_dir, "bin/rails g model User name:string")
|
266
|
+
|
267
|
+
# Set up authentication
|
268
|
+
setup_rails_authentication(rails_app_dir, gemfile_content)
|
269
|
+
|
270
|
+
# Use safe_rails_command for migrations
|
271
|
+
safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=development")
|
272
|
+
safe_rails_command(rails_app_dir, "bin/rails db:migrate RAILS_ENV=test")
|
273
|
+
|
274
|
+
puts "Rails application created successfully with authentication!"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
# rubocop:enable Metrics/BlockLength
|
278
|
+
# Tasks for the dummy Rails app
|
279
|
+
namespace :dummy do
|
280
|
+
desc "Prepare the dummy Rails app for testing"
|
281
|
+
task prepare: :environment do
|
282
|
+
app_path = File.expand_path("spec/dummy", __dir__)
|
283
|
+
|
284
|
+
# Ensure the db directory exists
|
285
|
+
FileUtils.mkdir_p(File.join(app_path, "db"))
|
286
|
+
|
287
|
+
# Set up environment
|
288
|
+
ENV["RAILS_ENV"] = "test"
|
289
|
+
|
290
|
+
# Make sure we can load ActiveRecord
|
291
|
+
require "active_record"
|
292
|
+
|
293
|
+
# Configure ActiveRecord for in-memory SQLite
|
294
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
295
|
+
|
296
|
+
# Load the schema
|
297
|
+
ActiveRecord::Schema.verbose = false
|
298
|
+
load File.expand_path("spec/dummy/db/schema.rb", __dir__)
|
299
|
+
|
300
|
+
Rails.logger.debug "Dummy Rails app prepared successfully!"
|
301
|
+
end
|
302
|
+
|
303
|
+
desc "Run RSpec tests with the dummy Rails app"
|
304
|
+
task tests: :prepare do
|
305
|
+
ENV["RAILS_ENV"] = "test"
|
306
|
+
Rake::Task["spec"].invoke
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Task to cleanup the dummy Rails app
|
311
|
+
task clean: :environment do
|
312
|
+
app_path = File.expand_path("spec/dummy", __dir__)
|
313
|
+
db_path = File.join(app_path, "db", "test.sqlite3")
|
314
|
+
FileUtils.rm_f(db_path)
|
315
|
+
end
|
316
|
+
|
317
|
+
begin
|
318
|
+
require "rubocop/rake_task"
|
319
|
+
RuboCop::RakeTask.new
|
320
|
+
rescue LoadError
|
321
|
+
desc "Run RuboCop"
|
322
|
+
task rubocop: :environment do
|
323
|
+
abort "RuboCop is not available. Run 'bundle install' first."
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
begin
|
328
|
+
require "brakeman"
|
329
|
+
desc "Run Brakeman"
|
330
|
+
task brakeman: :environment do
|
331
|
+
Brakeman.run(app_path: ".")
|
332
|
+
end
|
333
|
+
rescue LoadError
|
334
|
+
desc "Run Brakeman"
|
335
|
+
task brakeman: :environment do
|
336
|
+
abort "Brakeman is not available. Run 'bundle install' first."
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
desc "Run all CI checks"
|
341
|
+
task ci: %i[rubocop all_tests brakeman]
|
data/UPGRADE.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Clavis Upgrade Guide
|
2
|
+
|
3
|
+
## Upgrading to 0.2.0 from 0.1.x
|
4
|
+
|
5
|
+
Version 0.2.0 brings several important fixes and improvements to the Clavis gem, focusing on better documentation, improved compatibility with existing Rails applications, and bug fixes for various integration issues.
|
6
|
+
|
7
|
+
### Key Changes
|
8
|
+
|
9
|
+
1. **Module Alias Consistency**:
|
10
|
+
- The module `Clavis::Models::OauthAuthenticatable` is now properly aliased to `Clavis::Models::Concerns::OauthAuthenticatable`
|
11
|
+
- This means either reference can be used in your application code: `include Clavis::Models::OauthAuthenticatable` or `include Clavis::Models::Concerns::OauthAuthenticatable`
|
12
|
+
|
13
|
+
2. **View Helper Integration**:
|
14
|
+
- Improved automatic inclusion of view helpers in Rails applications
|
15
|
+
- Fixed issues with helper availability in `ApplicationHelper`
|
16
|
+
- View helpers like `clavis_oauth_button` should now work out of the box
|
17
|
+
|
18
|
+
3. **Installation Generator Improvements**:
|
19
|
+
- Updated the `clavis:install` generator to properly handle the creation of OAuth identity tables
|
20
|
+
- Fixed issues with duplicate migrations
|
21
|
+
- Added better support for integrating with existing User models
|
22
|
+
|
23
|
+
4. **Documentation Updates**:
|
24
|
+
- Added comprehensive guide for integrating with existing authentication systems
|
25
|
+
- Corrected inconsistencies between code examples and actual implementations
|
26
|
+
- Updated installation and configuration guidance
|
27
|
+
|
28
|
+
### Migration Steps
|
29
|
+
|
30
|
+
**For most users, no changes are required**. The improvements in 0.2.0 are backward compatible.
|
31
|
+
|
32
|
+
If you encountered any of the specific issues addressed in this release, upgrade to 0.2.0 and follow these steps:
|
33
|
+
|
34
|
+
1. Update your Gemfile:
|
35
|
+
```ruby
|
36
|
+
gem 'clavis', '~> 0.2.0'
|
37
|
+
```
|
38
|
+
|
39
|
+
2. Run bundle install:
|
40
|
+
```bash
|
41
|
+
bundle install
|
42
|
+
```
|
43
|
+
|
44
|
+
3. If you've manually implemented workarounds for any of the fixed issues, you can now remove them.
|
45
|
+
|
46
|
+
4. If you're integrating with an existing application, refer to the new integration guide at `/docs/integration.md` for detailed instructions.
|
47
|
+
|
48
|
+
### New Documentation
|
49
|
+
|
50
|
+
- `docs/integration.md` - Guide for integrating Clavis with existing authentication systems
|
51
|
+
- Updated README with clearer instructions and example code
|
52
|
+
- Improved error handling documentation
|
53
|
+
|
54
|
+
### Known Issues
|
55
|
+
|
56
|
+
- Rails 7.1+ and Ruby 3.4+ compatibility testing is ongoing
|
57
|
+
- Full test suite coverage requires a Rails environment for some specs
|
@@ -0,0 +1,133 @@
|
|
1
|
+
/* Clavis OAuth Button Styles */
|
2
|
+
|
3
|
+
.clavis-oauth-button {
|
4
|
+
display: inline-flex;
|
5
|
+
align-items: center;
|
6
|
+
justify-content: center;
|
7
|
+
padding: 10px 16px;
|
8
|
+
border-radius: 4px;
|
9
|
+
font-size: 14px;
|
10
|
+
font-weight: 500;
|
11
|
+
text-decoration: none;
|
12
|
+
margin: 5px 0;
|
13
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
14
|
+
transition: all 0.2s ease;
|
15
|
+
cursor: pointer;
|
16
|
+
min-width: 240px;
|
17
|
+
height: 40px;
|
18
|
+
box-sizing: border-box;
|
19
|
+
}
|
20
|
+
|
21
|
+
.clavis-oauth-button__icon,
|
22
|
+
.clavis-oauth-button span .clavis-icon {
|
23
|
+
width: 18px;
|
24
|
+
height: 18px;
|
25
|
+
margin-right: 10px;
|
26
|
+
fill: currentColor;
|
27
|
+
}
|
28
|
+
|
29
|
+
.clavis-oauth-button span {
|
30
|
+
line-height: 1;
|
31
|
+
}
|
32
|
+
|
33
|
+
/* Google - Following Google branding guidelines */
|
34
|
+
.clavis-oauth-button--google {
|
35
|
+
background-color: white;
|
36
|
+
color: rgba(0, 0, 0, 0.54);
|
37
|
+
border: 1px solid #dadce0;
|
38
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
39
|
+
font-weight: 500;
|
40
|
+
}
|
41
|
+
|
42
|
+
.clavis-oauth-button--google:hover {
|
43
|
+
background-color: #f8f8f8;
|
44
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
45
|
+
}
|
46
|
+
|
47
|
+
.clavis-oauth-button--google:focus {
|
48
|
+
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.3);
|
49
|
+
outline: none;
|
50
|
+
}
|
51
|
+
|
52
|
+
.clavis-oauth-button--google .clavis-icon {
|
53
|
+
width: 18px;
|
54
|
+
height: 18px;
|
55
|
+
}
|
56
|
+
|
57
|
+
/* GitHub */
|
58
|
+
.clavis-oauth-button--github {
|
59
|
+
background-color: #24292e;
|
60
|
+
color: white;
|
61
|
+
}
|
62
|
+
|
63
|
+
.clavis-oauth-button--github:hover {
|
64
|
+
background-color: #2c3238;
|
65
|
+
border-color: #24292e;
|
66
|
+
}
|
67
|
+
|
68
|
+
/* Apple - Following Apple's Sign in with Apple guidelines */
|
69
|
+
.clavis-oauth-button--apple {
|
70
|
+
background-color: black;
|
71
|
+
color: white;
|
72
|
+
border-radius: 4px;
|
73
|
+
}
|
74
|
+
|
75
|
+
.clavis-oauth-button--apple:hover {
|
76
|
+
background-color: #333;
|
77
|
+
}
|
78
|
+
|
79
|
+
.clavis-oauth-button--apple .clavis-icon {
|
80
|
+
width: 16px;
|
81
|
+
height: 16px;
|
82
|
+
}
|
83
|
+
|
84
|
+
/* Facebook - Following Facebook branding guidelines */
|
85
|
+
.clavis-oauth-button--facebook {
|
86
|
+
background-color: #1877F2;
|
87
|
+
color: white;
|
88
|
+
border: none;
|
89
|
+
font-weight: bold;
|
90
|
+
}
|
91
|
+
|
92
|
+
.clavis-oauth-button--facebook:hover {
|
93
|
+
background-color: #166fe5;
|
94
|
+
border-color: #166fe5;
|
95
|
+
}
|
96
|
+
|
97
|
+
/* Microsoft - Following Microsoft branding guidelines */
|
98
|
+
.clavis-oauth-button--microsoft {
|
99
|
+
background-color: white;
|
100
|
+
color: #5e5e5e;
|
101
|
+
border: 1px solid #8c8c8c;
|
102
|
+
}
|
103
|
+
|
104
|
+
.clavis-oauth-button--microsoft:hover {
|
105
|
+
background-color: #f0f0f0;
|
106
|
+
}
|
107
|
+
|
108
|
+
.clavis-oauth-button--microsoft .clavis-icon {
|
109
|
+
width: 16px;
|
110
|
+
height: 16px;
|
111
|
+
}
|
112
|
+
|
113
|
+
/* Generic OAuth button */
|
114
|
+
.clavis-oauth-button--oauth {
|
115
|
+
background-color: #f8f9fa;
|
116
|
+
color: #202124;
|
117
|
+
border: 1px solid #dadce0;
|
118
|
+
}
|
119
|
+
|
120
|
+
.clavis-oauth-button--oauth:hover {
|
121
|
+
background-color: #f1f3f4;
|
122
|
+
}
|
123
|
+
|
124
|
+
/* Error message */
|
125
|
+
.clavis-error {
|
126
|
+
color: #721c24;
|
127
|
+
background-color: #f8d7da;
|
128
|
+
border: 1px solid #f5c6cb;
|
129
|
+
padding: 10px;
|
130
|
+
border-radius: 4px;
|
131
|
+
margin: 10px 0;
|
132
|
+
font-size: 14px;
|
133
|
+
}
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clavis
|
4
|
+
# AuthController directly inherits from ActionController::Base to avoid inheriting
|
5
|
+
# host application's authentication requirements or before_actions that could
|
6
|
+
# interfere with the OAuth flow
|
7
|
+
class AuthController < ::ActionController::Base
|
8
|
+
include Clavis::Controllers::Concerns::Authentication
|
9
|
+
include Clavis::Controllers::Concerns::SessionManagement
|
10
|
+
|
11
|
+
# Add basic controller setup
|
12
|
+
protect_from_forgery with: :exception
|
13
|
+
|
14
|
+
# Allow access to main_app routes
|
15
|
+
helper Rails.application.routes.url_helpers if defined?(Rails)
|
16
|
+
|
17
|
+
# Skip CSRF protection for OAuth callback endpoints since they come from external redirects
|
18
|
+
skip_before_action :verify_authenticity_token, only: [:callback]
|
19
|
+
|
20
|
+
def authorize
|
21
|
+
# Store the current URL for returning after authentication
|
22
|
+
store_location if request.get?
|
23
|
+
|
24
|
+
oauth_authorize
|
25
|
+
end
|
26
|
+
|
27
|
+
def callback
|
28
|
+
oauth_callback do |user, auth_hash|
|
29
|
+
# This is a default implementation that can be overridden
|
30
|
+
# by the application using the gem
|
31
|
+
if respond_to?(:clavis_authentication_success)
|
32
|
+
clavis_authentication_success(user, auth_hash)
|
33
|
+
else
|
34
|
+
# Use the SessionManagement method to sign in the user with secure cookies
|
35
|
+
sign_in_user(user)
|
36
|
+
|
37
|
+
# Get the redirect path
|
38
|
+
redirect_path = after_login_path
|
39
|
+
|
40
|
+
# Force redirect to root path if it's redirecting to auth path
|
41
|
+
if redirect_path.include?("/auth/")
|
42
|
+
redirect_path = defined?(main_app) && main_app.respond_to?(:root_path) ? main_app.root_path : "/"
|
43
|
+
end
|
44
|
+
|
45
|
+
redirect_to redirect_path, notice: "Successfully signed in with #{params[:provider].capitalize}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rescue StandardError => e
|
49
|
+
Clavis::Logging.log_error(e)
|
50
|
+
|
51
|
+
if respond_to?(:clavis_authentication_failure)
|
52
|
+
clavis_authentication_failure(e)
|
53
|
+
else
|
54
|
+
# Default behavior: redirect to sign in page with error
|
55
|
+
flash[:alert] = case e
|
56
|
+
when Clavis::AuthorizationDenied
|
57
|
+
"Authentication was cancelled"
|
58
|
+
when Clavis::InvalidState, Clavis::MissingState
|
59
|
+
"Authentication session expired. Please try again."
|
60
|
+
else
|
61
|
+
"Authentication failed: #{e.message}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Safe redirect to after_login_path
|
65
|
+
redirect_to after_login_path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def find_or_create_user_from_oauth(auth_hash)
|
72
|
+
user_class = Clavis.configuration.user_class.constantize
|
73
|
+
finder_method = Clavis.configuration.user_finder_method
|
74
|
+
|
75
|
+
if user_class.respond_to?(finder_method)
|
76
|
+
user_class.public_send(finder_method, auth_hash)
|
77
|
+
else
|
78
|
+
# If no suitable method is available, just return the auth hash
|
79
|
+
auth_hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Method to handle authentication requests with proper routing
|
84
|
+
def request_authentication
|
85
|
+
session[:return_to_after_authenticating] = request.url
|
86
|
+
|
87
|
+
# Only redirect to paths we control or to root_path
|
88
|
+
# This avoids assumptions about the host application's routes
|
89
|
+
redirect_to main_app.root_path, alert: "Authentication required. Please sign in to continue."
|
90
|
+
end
|
91
|
+
|
92
|
+
def after_authentication_url
|
93
|
+
return session.delete(:return_to_after_authenticating) if session[:return_to_after_authenticating].present?
|
94
|
+
|
95
|
+
# Try to get main_app's root_path, fall back to "/"
|
96
|
+
begin
|
97
|
+
main_app.respond_to?(:root_path) ? main_app.root_path : "/"
|
98
|
+
rescue StandardError
|
99
|
+
"/"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def safe_redirect(path = nil)
|
104
|
+
# Try default paths in order of preference
|
105
|
+
fallback_paths = [
|
106
|
+
-> { main_app.root_path if main_app.respond_to?(:root_path) },
|
107
|
+
-> { main_app.respond_to?(:login_path) ? main_app.login_path : nil },
|
108
|
+
-> { "/" } # Final fallback is always root
|
109
|
+
]
|
110
|
+
|
111
|
+
# Use provided path or find first working fallback
|
112
|
+
target_path = path || fallback_paths.lazy.map(&:call).find(&:present?)
|
113
|
+
|
114
|
+
# Perform the redirect with exception handling
|
115
|
+
begin
|
116
|
+
redirect_to target_path
|
117
|
+
rescue StandardError => e
|
118
|
+
# Log the error and redirect to root as ultimate fallback
|
119
|
+
Clavis::Logging.log_error("Redirect error: #{e.message}. Falling back to '/'")
|
120
|
+
redirect_to "/"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Override default_path to ensure we don't redirect back to auth paths
|
125
|
+
def default_path
|
126
|
+
if defined?(main_app) && main_app.respond_to?(:root_path)
|
127
|
+
main_app.root_path
|
128
|
+
else
|
129
|
+
"/"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/config/database.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
default: &default
|
2
|
+
adapter: sqlite3
|
3
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
4
|
+
timeout: 5000
|
5
|
+
|
6
|
+
development:
|
7
|
+
<<: *default
|
8
|
+
database: db/development.sqlite3
|
9
|
+
|
10
|
+
test:
|
11
|
+
<<: *default
|
12
|
+
database: ":memory:"
|
13
|
+
|
14
|
+
production:
|
15
|
+
<<: *default
|
16
|
+
database: db/production.sqlite3
|