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/docs/integration.md
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
# Integrating Clavis with Existing Applications
|
2
|
+
|
3
|
+
This guide covers how to integrate Clavis with your existing Ruby on Rails application, particularly if you already have an authentication system in place.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
1. [Overview](#overview)
|
8
|
+
2. [Database Setup](#database-setup)
|
9
|
+
3. [User Model Integration](#user-model-integration)
|
10
|
+
4. [Controller Integration](#controller-integration)
|
11
|
+
5. [View Integration](#view-integration)
|
12
|
+
6. [Route Configuration](#route-configuration)
|
13
|
+
7. [Working with Multiple Authentication Methods](#working-with-multiple-authentication-methods)
|
14
|
+
8. [Troubleshooting](#troubleshooting)
|
15
|
+
|
16
|
+
## Overview
|
17
|
+
|
18
|
+
Clavis is designed to work alongside your existing authentication system, providing OAuth/OIDC capabilities without replacing your current setup. This guide assumes you have an existing application with:
|
19
|
+
|
20
|
+
- A `User` model
|
21
|
+
- Some form of authentication (e.g., `has_secure_password`, Devise, etc.)
|
22
|
+
- Session management
|
23
|
+
|
24
|
+
## Database Setup
|
25
|
+
|
26
|
+
Clavis stores OAuth identities in a separate table with a polymorphic relationship to your user model.
|
27
|
+
|
28
|
+
1. **Run the installation generator**:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
rails generate clavis:install
|
32
|
+
```
|
33
|
+
|
34
|
+
2. **Review and run the migration**:
|
35
|
+
|
36
|
+
```bash
|
37
|
+
rails db:migrate
|
38
|
+
```
|
39
|
+
|
40
|
+
This creates a `clavis_oauth_identities` table with:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
create_table :clavis_oauth_identities do |t|
|
44
|
+
t.references :user, polymorphic: true, null: false, index: true
|
45
|
+
t.string :provider, null: false
|
46
|
+
t.string :uid, null: false
|
47
|
+
t.json :auth_data
|
48
|
+
t.string :token
|
49
|
+
t.string :refresh_token
|
50
|
+
t.datetime :expires_at
|
51
|
+
t.timestamps
|
52
|
+
|
53
|
+
t.index [:provider, :uid], unique: true
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
## User Model Integration
|
58
|
+
|
59
|
+
Include the `OauthAuthenticatable` concern in your User model:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# app/models/user.rb
|
63
|
+
class User < ApplicationRecord
|
64
|
+
include Clavis::Models::OauthAuthenticatable
|
65
|
+
|
66
|
+
# Your existing authentication code (e.g., has_secure_password)
|
67
|
+
|
68
|
+
# Optional: Customize how users are created/found from OAuth data
|
69
|
+
def self.find_for_oauth(auth_hash)
|
70
|
+
super do |user, auth|
|
71
|
+
# Set additional attributes based on the OAuth data
|
72
|
+
user.name = auth[:info][:name] if user.respond_to?(:name=)
|
73
|
+
user.username = auth[:info][:nickname] if user.respond_to?(:username=)
|
74
|
+
# You can access profile image with auth[:info][:image]
|
75
|
+
# You can access email with auth[:info][:email]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
## Controller Integration
|
82
|
+
|
83
|
+
You have a few options for controller integration:
|
84
|
+
|
85
|
+
### Option 1: Use your existing authentication controller
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# app/controllers/sessions_controller.rb
|
89
|
+
class SessionsController < ApplicationController
|
90
|
+
include Clavis::Controllers::Concerns::Authentication
|
91
|
+
|
92
|
+
# Your existing login/logout actions...
|
93
|
+
|
94
|
+
# Add OAuth callback handler
|
95
|
+
def oauth_callback
|
96
|
+
auth_hash = process_callback(params[:provider])
|
97
|
+
|
98
|
+
# Find or create a user with the OAuth data
|
99
|
+
@user = User.find_for_oauth(auth_hash)
|
100
|
+
|
101
|
+
# Sign in the user (using your existing authentication system)
|
102
|
+
session[:user_id] = @user.id
|
103
|
+
|
104
|
+
redirect_to root_path, notice: "Signed in successfully!"
|
105
|
+
rescue Clavis::AuthenticationError => e
|
106
|
+
redirect_to login_path, alert: "Authentication failed: #{e.message}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add OAuth authorization handler
|
110
|
+
def oauth_authorize
|
111
|
+
redirect_to auth_url(params[:provider])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Option 2: Generate a dedicated OAuth controller
|
117
|
+
|
118
|
+
```bash
|
119
|
+
rails generate clavis:controller Auth
|
120
|
+
```
|
121
|
+
|
122
|
+
This creates:
|
123
|
+
- `app/controllers/auth_controller.rb` with OAuth methods
|
124
|
+
- Views for login/etc.
|
125
|
+
- Routes for the OAuth flow
|
126
|
+
|
127
|
+
## View Integration
|
128
|
+
|
129
|
+
### Add OAuth buttons to your login form:
|
130
|
+
|
131
|
+
```erb
|
132
|
+
<%# app/views/sessions/new.html.erb %>
|
133
|
+
<h1>Sign In</h1>
|
134
|
+
|
135
|
+
<%# Your existing login form... %>
|
136
|
+
|
137
|
+
<div class="oauth-buttons">
|
138
|
+
<p>Or sign in with:</p>
|
139
|
+
<%= clavis_oauth_button :google %>
|
140
|
+
<%= clavis_oauth_button :github %>
|
141
|
+
</div>
|
142
|
+
```
|
143
|
+
|
144
|
+
### Customize button appearance:
|
145
|
+
|
146
|
+
```erb
|
147
|
+
<%= clavis_oauth_button :google, text: "Continue with Google", class: "my-custom-button" %>
|
148
|
+
```
|
149
|
+
|
150
|
+
## Route Configuration
|
151
|
+
|
152
|
+
Add the necessary routes to your application:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# config/routes.rb
|
156
|
+
Rails.application.routes.draw do
|
157
|
+
# Your existing routes...
|
158
|
+
|
159
|
+
# OAuth routes
|
160
|
+
get '/auth/:provider', to: 'sessions#oauth_authorize', as: :auth
|
161
|
+
get '/auth/:provider/callback', to: 'sessions#oauth_callback'
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Working with Multiple Authentication Methods
|
166
|
+
|
167
|
+
When a user signs in with OAuth, you'll need to decide how to handle users who might already have password-based accounts:
|
168
|
+
|
169
|
+
### Email Matching Strategy
|
170
|
+
|
171
|
+
This is the default strategy in Clavis - when a user signs in with OAuth:
|
172
|
+
|
173
|
+
1. Clavis tries to find an existing `OauthIdentity` for the provider/uid
|
174
|
+
2. If not found, it looks for a user with a matching email address
|
175
|
+
3. If a user with matching email is found, it associates the OAuth identity with that user
|
176
|
+
4. If no user is found, it creates a new user and associates the OAuth identity
|
177
|
+
|
178
|
+
### Linking Accounts
|
179
|
+
|
180
|
+
You might want to allow users to link multiple OAuth providers to their account:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
# app/controllers/profiles_controller.rb
|
184
|
+
def link_oauth
|
185
|
+
# Store the user_id in the session
|
186
|
+
session[:linking_user_id] = current_user.id
|
187
|
+
|
188
|
+
# Redirect to the OAuth provider
|
189
|
+
redirect_to auth_path(params[:provider])
|
190
|
+
end
|
191
|
+
|
192
|
+
# app/controllers/sessions_controller.rb
|
193
|
+
def oauth_callback
|
194
|
+
auth_hash = process_callback(params[:provider])
|
195
|
+
|
196
|
+
# Check if we're linking an existing account
|
197
|
+
if session[:linking_user_id].present?
|
198
|
+
user = User.find(session[:linking_user_id])
|
199
|
+
session.delete(:linking_user_id)
|
200
|
+
|
201
|
+
# Find or create the identity
|
202
|
+
identity = Clavis::OauthIdentity.find_or_initialize_by(
|
203
|
+
provider: auth_hash[:provider],
|
204
|
+
uid: auth_hash[:uid]
|
205
|
+
)
|
206
|
+
|
207
|
+
# Associate with the user
|
208
|
+
identity.user = user
|
209
|
+
identity.auth_data = auth_hash[:info]
|
210
|
+
identity.token = auth_hash[:credentials][:token]
|
211
|
+
identity.refresh_token = auth_hash[:credentials][:refresh_token]
|
212
|
+
identity.expires_at = auth_hash[:credentials][:expires_at] ? Time.at(auth_hash[:credentials][:expires_at]) : nil
|
213
|
+
identity.save!
|
214
|
+
|
215
|
+
redirect_to edit_profile_path, notice: "Successfully linked #{params[:provider].capitalize} account"
|
216
|
+
else
|
217
|
+
# Normal login flow...
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
## Troubleshooting
|
223
|
+
|
224
|
+
### View Helper Issues
|
225
|
+
|
226
|
+
If you're having trouble with the `clavis_oauth_button` helper, ensure your application helper includes Clavis's view helpers:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
# app/helpers/application_helper.rb
|
230
|
+
module ApplicationHelper
|
231
|
+
include Clavis::ViewHelpers
|
232
|
+
# ...
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
### Database Issues
|
237
|
+
|
238
|
+
If you see errors about the `clavis_oauth_identities` table, make sure you've run:
|
239
|
+
|
240
|
+
```bash
|
241
|
+
rails db:migrate
|
242
|
+
```
|
243
|
+
|
244
|
+
### Session Issues
|
245
|
+
|
246
|
+
If you're experiencing session-related issues:
|
247
|
+
|
248
|
+
1. Ensure you're not using `session.clear` which would remove Clavis's state parameters
|
249
|
+
2. Consider enabling session rotation in your Clavis configuration:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
Clavis.configure do |config|
|
253
|
+
config.rotate_session_after_login = true
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
### Missing Routes
|
258
|
+
|
259
|
+
If you see errors about missing routes for OAuth buttons, ensure you've added:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
get '/auth/:provider', to: 'sessions#oauth_authorize', as: :auth
|
263
|
+
get '/auth/:provider/callback', to: 'sessions#oauth_callback'
|
264
|
+
```
|
265
|
+
|
266
|
+
### Security Concerns
|
267
|
+
|
268
|
+
For production environments, always ensure:
|
269
|
+
|
270
|
+
1. You're using HTTPS
|
271
|
+
2. Your OAuth provider credentials are properly secured (e.g., using Rails credentials)
|
272
|
+
3. You've configured allowed redirect hosts in Clavis configuration
|
data/error_handling.md
ADDED
@@ -0,0 +1,355 @@
|
|
1
|
+
# Clavis Error Handling Strategy
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Clavis implements a structured error handling system that provides:
|
6
|
+
|
7
|
+
1. Clear, descriptive error messages
|
8
|
+
2. Specific error types for different failures
|
9
|
+
3. Rails logger integration
|
10
|
+
4. Easy error rescue patterns for application code
|
11
|
+
|
12
|
+
## Error Hierarchy
|
13
|
+
|
14
|
+
```
|
15
|
+
Clavis::Error (base class)
|
16
|
+
├── ConfigurationError
|
17
|
+
│ ├── ProviderNotConfigured
|
18
|
+
│ └── MissingConfiguration
|
19
|
+
├── AuthenticationError
|
20
|
+
│ ├── InvalidState
|
21
|
+
│ ├── MissingState
|
22
|
+
│ └── AuthorizationDenied
|
23
|
+
├── TokenError
|
24
|
+
│ ├── InvalidToken
|
25
|
+
│ ├── ExpiredToken
|
26
|
+
│ ├── InvalidGrant
|
27
|
+
│ └── InvalidAccessToken
|
28
|
+
└── ProviderError
|
29
|
+
├── UnsupportedProvider
|
30
|
+
└── ProviderAPIError
|
31
|
+
```
|
32
|
+
|
33
|
+
## Error Classes
|
34
|
+
|
35
|
+
### Configuration Errors
|
36
|
+
|
37
|
+
- **ConfigurationError**: Base class for configuration-related errors
|
38
|
+
- **ProviderNotConfigured**: Raised when trying to use an unconfigured provider
|
39
|
+
- **MissingConfiguration**: Raised when required configuration values are missing
|
40
|
+
|
41
|
+
### Authentication Errors
|
42
|
+
|
43
|
+
- **AuthenticationError**: Base class for authentication flow errors
|
44
|
+
- **InvalidState**: Raised when state parameter validation fails (CSRF protection)
|
45
|
+
- **MissingState**: Raised when state parameter is missing from the session
|
46
|
+
- **AuthorizationDenied**: Raised when the user denies authorization at the provider
|
47
|
+
|
48
|
+
### Token Errors
|
49
|
+
|
50
|
+
- **TokenError**: Base class for token-related errors
|
51
|
+
- **InvalidToken**: Raised when token validation fails
|
52
|
+
- **ExpiredToken**: Raised when a token has expired
|
53
|
+
- **InvalidGrant**: Raised when an authorization code is invalid or expired
|
54
|
+
- **InvalidAccessToken**: Raised when using an invalid access token
|
55
|
+
|
56
|
+
### Provider Errors
|
57
|
+
|
58
|
+
- **ProviderError**: Base class for provider-related errors
|
59
|
+
- **UnsupportedProvider**: Raised when trying to use an unsupported provider
|
60
|
+
- **ProviderAPIError**: Raised when a provider API returns an error
|
61
|
+
|
62
|
+
## Implementation
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# lib/clavis/errors.rb
|
66
|
+
module Clavis
|
67
|
+
# Base error class
|
68
|
+
class Error < StandardError
|
69
|
+
def initialize(message = nil)
|
70
|
+
@message = message
|
71
|
+
super(format_message)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def format_message
|
77
|
+
return @message if @message
|
78
|
+
|
79
|
+
class_name = self.class.name.split('::').last
|
80
|
+
words = class_name.gsub(/([A-Z])/, ' \1').strip.split(' ')
|
81
|
+
words.join(' ').downcase
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Configuration errors
|
86
|
+
class ConfigurationError < Error; end
|
87
|
+
|
88
|
+
class ProviderNotConfigured < ConfigurationError
|
89
|
+
def initialize(provider)
|
90
|
+
@provider = provider
|
91
|
+
super("Provider '#{provider}' is not configured")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class MissingConfiguration < ConfigurationError
|
96
|
+
def initialize(option)
|
97
|
+
@option = option
|
98
|
+
super("Missing required configuration option: #{option}")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Authentication errors
|
103
|
+
class AuthenticationError < Error; end
|
104
|
+
|
105
|
+
class InvalidState < AuthenticationError
|
106
|
+
def initialize
|
107
|
+
super("Invalid state parameter. This may be a CSRF attempt or the session expired")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class MissingState < AuthenticationError
|
112
|
+
def initialize
|
113
|
+
super("Missing state parameter in session. Session may have expired")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class AuthorizationDenied < AuthenticationError
|
118
|
+
def initialize(reason = nil)
|
119
|
+
@reason = reason
|
120
|
+
super(reason ? "Authorization denied: #{reason}" : "Authorization denied by user")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Token errors
|
125
|
+
class TokenError < Error; end
|
126
|
+
|
127
|
+
class InvalidToken < TokenError
|
128
|
+
def initialize(reason = nil)
|
129
|
+
@reason = reason
|
130
|
+
super(reason ? "Invalid token: #{reason}" : "Token validation failed")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class ExpiredToken < TokenError
|
135
|
+
def initialize
|
136
|
+
super("Token has expired")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class InvalidGrant < TokenError
|
141
|
+
def initialize(reason = nil)
|
142
|
+
@reason = reason
|
143
|
+
super(reason ? "Invalid grant: #{reason}" : "Authorization code is invalid or expired")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class InvalidAccessToken < TokenError
|
148
|
+
def initialize
|
149
|
+
super("Access token is invalid or expired")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Provider errors
|
154
|
+
class ProviderError < Error; end
|
155
|
+
|
156
|
+
class UnsupportedProvider < ProviderError
|
157
|
+
def initialize(provider)
|
158
|
+
@provider = provider
|
159
|
+
super("Provider '#{provider}' is not supported")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class ProviderAPIError < ProviderError
|
164
|
+
def initialize(provider, error = nil)
|
165
|
+
@provider = provider
|
166
|
+
@error = error
|
167
|
+
message = "Error from #{provider} API"
|
168
|
+
message += ": #{error}" if error
|
169
|
+
super(message)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
## Logging Integration
|
176
|
+
|
177
|
+
All errors are automatically logged with appropriate log levels:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
# lib/clavis/logging.rb
|
181
|
+
module Clavis
|
182
|
+
module Logging
|
183
|
+
def self.log_error(error)
|
184
|
+
case error
|
185
|
+
when Clavis::AuthorizationDenied
|
186
|
+
# User chose to cancel, not a real error
|
187
|
+
Rails.logger.info("[Clavis] Authorization denied: #{error.message}")
|
188
|
+
when Clavis::InvalidState, Clavis::MissingState
|
189
|
+
# Could be session expiration or CSRF attempt
|
190
|
+
Rails.logger.warn("[Clavis] Security issue: #{error.class.name} - #{error.message}")
|
191
|
+
when Clavis::ProviderAPIError
|
192
|
+
# Provider API errors
|
193
|
+
Rails.logger.error("[Clavis] Provider API error: #{error.message}")
|
194
|
+
when Clavis::ConfigurationError
|
195
|
+
# Configuration issues
|
196
|
+
Rails.logger.error("[Clavis] Configuration error: #{error.message}")
|
197
|
+
else
|
198
|
+
# All other errors
|
199
|
+
Rails.logger.error("[Clavis] #{error.class.name}: #{error.message}")
|
200
|
+
end
|
201
|
+
|
202
|
+
# Only log backtraces for unexpected errors in debug mode
|
203
|
+
unless error.is_a?(Clavis::AuthorizationDenied)
|
204
|
+
Rails.logger.debug("[Clavis] #{error.backtrace.join("\n")}")
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
## Error Handling in Controllers
|
212
|
+
|
213
|
+
Example of how to handle Clavis errors in a controller:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# app/controllers/sessions_controller.rb
|
217
|
+
class SessionsController < ApplicationController
|
218
|
+
include Clavis::Controllers::Concerns::Authentication
|
219
|
+
|
220
|
+
def create_from_oauth
|
221
|
+
oauth_callback do |user, auth_hash|
|
222
|
+
session[:user_id] = user.id
|
223
|
+
redirect_to root_path, notice: "Signed in successfully!"
|
224
|
+
end
|
225
|
+
rescue Clavis::AuthorizationDenied
|
226
|
+
# User cancelled the authentication
|
227
|
+
redirect_to login_path, notice: "Authentication cancelled"
|
228
|
+
rescue Clavis::InvalidState, Clavis::MissingState
|
229
|
+
# Session expired or possible CSRF attempt
|
230
|
+
redirect_to login_path, alert: "Authentication session expired. Please try again."
|
231
|
+
rescue Clavis::TokenError => e
|
232
|
+
# Token-related errors
|
233
|
+
Clavis::Logging.log_error(e)
|
234
|
+
redirect_to login_path, alert: "Authentication failed. Please try again."
|
235
|
+
rescue Clavis::ProviderAPIError => e
|
236
|
+
# Provider API errors
|
237
|
+
Clavis::Logging.log_error(e)
|
238
|
+
redirect_to login_path, alert: "Service temporarily unavailable. Please try again later."
|
239
|
+
rescue Clavis::Error => e
|
240
|
+
# Catch all other Clavis errors
|
241
|
+
Clavis::Logging.log_error(e)
|
242
|
+
redirect_to login_path, alert: "Authentication failed: #{e.message}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
## Custom Error Pages
|
248
|
+
|
249
|
+
For a better user experience, consider creating custom error pages:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
# config/routes.rb
|
253
|
+
Rails.application.routes.draw do
|
254
|
+
# Authentication failure routes
|
255
|
+
get '/auth/failure', to: 'sessions#failure'
|
256
|
+
|
257
|
+
# Other routes...
|
258
|
+
end
|
259
|
+
|
260
|
+
# app/controllers/sessions_controller.rb
|
261
|
+
class SessionsController < ApplicationController
|
262
|
+
def failure
|
263
|
+
reason = params[:message] || "unknown reason"
|
264
|
+
redirect_to login_path, alert: "Authentication failed: #{reason}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
## Handling OAuth Error Responses
|
270
|
+
|
271
|
+
The OAuth 2.0 specification defines standard error responses. Clavis maps these to specific exceptions:
|
272
|
+
|
273
|
+
| OAuth Error | Clavis Exception |
|
274
|
+
|-------------|------------------|
|
275
|
+
| `invalid_request` | `Clavis::AuthenticationError` |
|
276
|
+
| `unauthorized_client` | `Clavis::AuthenticationError` |
|
277
|
+
| `access_denied` | `Clavis::AuthorizationDenied` |
|
278
|
+
| `unsupported_response_type` | `Clavis::ConfigurationError` |
|
279
|
+
| `invalid_scope` | `Clavis::ConfigurationError` |
|
280
|
+
| `server_error` | `Clavis::ProviderAPIError` |
|
281
|
+
| `temporarily_unavailable` | `Clavis::ProviderAPIError` |
|
282
|
+
| `invalid_client` | `Clavis::ConfigurationError` |
|
283
|
+
| `invalid_grant` | `Clavis::InvalidGrant` |
|
284
|
+
| `invalid_token` | `Clavis::InvalidToken` |
|
285
|
+
|
286
|
+
## Exception Middleware
|
287
|
+
|
288
|
+
For Rails API applications, consider adding a middleware to handle Clavis exceptions:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
# lib/clavis/middleware/exception_handler.rb
|
292
|
+
module Clavis
|
293
|
+
module Middleware
|
294
|
+
class ExceptionHandler
|
295
|
+
def initialize(app)
|
296
|
+
@app = app
|
297
|
+
end
|
298
|
+
|
299
|
+
def call(env)
|
300
|
+
@app.call(env)
|
301
|
+
rescue Clavis::Error => e
|
302
|
+
Clavis::Logging.log_error(e)
|
303
|
+
|
304
|
+
# Convert to appropriate HTTP response
|
305
|
+
case e
|
306
|
+
when Clavis::AuthorizationDenied
|
307
|
+
# User cancelled
|
308
|
+
[302, { 'Location' => '/auth/failure?message=denied' }, []]
|
309
|
+
when Clavis::InvalidState, Clavis::MissingState
|
310
|
+
# Security issues
|
311
|
+
[302, { 'Location' => '/auth/failure?message=session_expired' }, []]
|
312
|
+
when Clavis::TokenError
|
313
|
+
# Token issues
|
314
|
+
[302, { 'Location' => '/auth/failure?message=token_error' }, []]
|
315
|
+
when Clavis::ProviderAPIError
|
316
|
+
# Provider API issues
|
317
|
+
[302, { 'Location' => '/auth/failure?message=provider_error' }, []]
|
318
|
+
else
|
319
|
+
# Other Clavis errors
|
320
|
+
[302, { 'Location' => "/auth/failure?message=#{CGI.escape(e.message)}" }, []]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
328
|
+
## Testing Error Handling
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
# spec/error_handling_spec.rb
|
332
|
+
RSpec.describe "Error Handling" do
|
333
|
+
describe "AuthorizationDenied error" do
|
334
|
+
it "redirects to login path with appropriate message" do
|
335
|
+
# Simulate user denying authorization
|
336
|
+
get "/auth/google/callback", params: { error: "access_denied" }
|
337
|
+
|
338
|
+
expect(response).to redirect_to(login_path)
|
339
|
+
expect(flash[:notice]).to include("Authentication cancelled")
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe "InvalidState error" do
|
344
|
+
it "redirects to login path with session expired message" do
|
345
|
+
# Simulate CSRF attack with invalid state
|
346
|
+
get "/auth/google/callback", params: { code: "123", state: "invalid" }
|
347
|
+
|
348
|
+
expect(response).to redirect_to(login_path)
|
349
|
+
expect(flash[:alert]).to include("session expired")
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# More error handling tests...
|
354
|
+
end
|
355
|
+
```
|