clavis 0.7.1 ā 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.brakeman.ignore +1 -0
- data/CHANGELOG.md +27 -15
- data/README.md +374 -535
- data/Rakefile +52 -4
- data/gemfiles/rails_80.gemfile +1 -0
- data/lib/clavis/controllers/concerns/authentication.rb +183 -113
- data/lib/clavis/errors.rb +19 -0
- data/lib/clavis/logging.rb +53 -0
- data/lib/clavis/providers/apple.rb +249 -15
- data/lib/clavis/providers/base.rb +163 -75
- data/lib/clavis/providers/facebook.rb +123 -10
- data/lib/clavis/providers/github.rb +47 -10
- data/lib/clavis/providers/google.rb +189 -6
- data/lib/clavis/providers/token_exchange_handler.rb +125 -0
- data/lib/clavis/security/csrf_protection.rb +92 -8
- data/lib/clavis/security/session_manager.rb +10 -0
- data/lib/clavis/version.rb +1 -1
- data/lib/clavis.rb +5 -0
- data/lib/generators/clavis/install_generator.rb +34 -17
- data/lib/generators/clavis/templates/initializer.rb +4 -2
- data/lib/generators/clavis/user_method/user_method_generator.rb +5 -16
- data/llms.md +256 -347
- metadata +4 -5
- data/UPGRADE.md +0 -57
- data/docs/integration.md +0 -272
- data/testing_plan.md +0 -710
@@ -6,10 +6,14 @@ module Clavis
|
|
6
6
|
module Security
|
7
7
|
module CsrfProtection
|
8
8
|
class << self
|
9
|
+
# Delimiter used to separate state from HMAC
|
10
|
+
STATE_HMAC_DELIMITER = "::"
|
11
|
+
|
9
12
|
# Generates a secure random state token for CSRF protection
|
13
|
+
# @param length [Integer] The byte length for the token (resulting hex string will be twice this length)
|
10
14
|
# @return [String] A secure random state token
|
11
|
-
def generate_state
|
12
|
-
SecureRandom.hex(
|
15
|
+
def generate_state(length = 24)
|
16
|
+
SecureRandom.hex(length)
|
13
17
|
end
|
14
18
|
|
15
19
|
# Validates that the actual state matches the expected state
|
@@ -24,10 +28,19 @@ module Clavis
|
|
24
28
|
|
25
29
|
# Stores a state token in the Rails session
|
26
30
|
# @param controller [ActionController::Base] The controller instance
|
31
|
+
# @param expiry [Time, Integer] Optional expiration time (Time object or seconds from now)
|
32
|
+
# @param length [Integer] Optional byte length for the token
|
27
33
|
# @return [String] The generated state token
|
28
|
-
def store_state_in_session(controller)
|
29
|
-
state = generate_state
|
34
|
+
def store_state_in_session(controller, expiry = nil, length = 24)
|
35
|
+
state = generate_state(length)
|
30
36
|
controller.session[:oauth_state] = state
|
37
|
+
|
38
|
+
# Store expiration if provided
|
39
|
+
if expiry
|
40
|
+
expiry_time = expiry.is_a?(Integer) ? Time.now.to_i + expiry : expiry.to_i
|
41
|
+
controller.session[:oauth_state_expiry] = expiry_time
|
42
|
+
end
|
43
|
+
|
31
44
|
state
|
32
45
|
end
|
33
46
|
|
@@ -36,26 +49,48 @@ module Clavis
|
|
36
49
|
# @param actual_state [String] The state received from the OAuth provider
|
37
50
|
# @raise [Clavis::MissingState] If either state is nil
|
38
51
|
# @raise [Clavis::InvalidState] If the states don't match
|
52
|
+
# @raise [Clavis::ExpiredState] If the state token has expired
|
39
53
|
def validate_state_from_session!(controller, actual_state)
|
40
54
|
expected_state = controller.session[:oauth_state]
|
55
|
+
expiry = controller.session[:oauth_state_expiry]
|
56
|
+
|
57
|
+
# Check for expiration if an expiry was set
|
58
|
+
if expiry && Time.now.to_i > expiry
|
59
|
+
# Clear expired state from session
|
60
|
+
controller.session.delete(:oauth_state)
|
61
|
+
controller.session.delete(:oauth_state_expiry)
|
62
|
+
raise Clavis::ExpiredState
|
63
|
+
end
|
64
|
+
|
41
65
|
validate_state!(actual_state, expected_state)
|
42
66
|
|
43
67
|
# Clear the state from the session after validation
|
44
68
|
controller.session.delete(:oauth_state)
|
69
|
+
controller.session.delete(:oauth_state_expiry)
|
45
70
|
end
|
46
71
|
|
47
72
|
# Generates a nonce for OIDC requests
|
73
|
+
# @param length [Integer] The byte length for the nonce (resulting hex string will be twice this length)
|
48
74
|
# @return [String] A secure random nonce
|
49
|
-
def generate_nonce
|
50
|
-
SecureRandom.hex(
|
75
|
+
def generate_nonce(length = 16)
|
76
|
+
SecureRandom.hex(length)
|
51
77
|
end
|
52
78
|
|
53
79
|
# Stores a nonce in the Rails session
|
54
80
|
# @param controller [ActionController::Base] The controller instance
|
81
|
+
# @param expiry [Time, Integer] Optional expiration time (Time object or seconds from now)
|
82
|
+
# @param length [Integer] Optional byte length for the nonce
|
55
83
|
# @return [String] The generated nonce
|
56
|
-
def store_nonce_in_session(controller)
|
57
|
-
nonce = generate_nonce
|
84
|
+
def store_nonce_in_session(controller, expiry = nil, length = 16)
|
85
|
+
nonce = generate_nonce(length)
|
58
86
|
controller.session[:oauth_nonce] = nonce
|
87
|
+
|
88
|
+
# Store expiration if provided
|
89
|
+
if expiry
|
90
|
+
expiry_time = expiry.is_a?(Integer) ? Time.now.to_i + expiry : expiry.to_i
|
91
|
+
controller.session[:oauth_nonce_expiry] = expiry_time
|
92
|
+
end
|
93
|
+
|
59
94
|
nonce
|
60
95
|
end
|
61
96
|
|
@@ -64,14 +99,63 @@ module Clavis
|
|
64
99
|
# @param id_token_nonce [String] The nonce from the ID token
|
65
100
|
# @raise [Clavis::MissingNonce] If either nonce is nil
|
66
101
|
# @raise [Clavis::InvalidNonce] If the nonces don't match
|
102
|
+
# @raise [Clavis::ExpiredState] If the nonce has expired
|
67
103
|
def validate_nonce_from_session!(controller, id_token_nonce)
|
68
104
|
expected_nonce = controller.session[:oauth_nonce]
|
105
|
+
expiry = controller.session[:oauth_nonce_expiry]
|
106
|
+
|
107
|
+
# Check for expiration if an expiry was set
|
108
|
+
if expiry && Time.now.to_i > expiry
|
109
|
+
# Clear expired nonce from session
|
110
|
+
controller.session.delete(:oauth_nonce)
|
111
|
+
controller.session.delete(:oauth_nonce_expiry)
|
112
|
+
raise Clavis::ExpiredState
|
113
|
+
end
|
69
114
|
|
70
115
|
raise Clavis::MissingNonce if id_token_nonce.nil? || expected_nonce.nil?
|
71
116
|
raise Clavis::InvalidNonce unless id_token_nonce == expected_nonce
|
72
117
|
|
73
118
|
# Clear the nonce from the session after validation
|
74
119
|
controller.session.delete(:oauth_nonce)
|
120
|
+
controller.session.delete(:oauth_nonce_expiry)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Binds a state token to the session context for extra security
|
124
|
+
# @param controller [ActionController::Base] The controller instance
|
125
|
+
# @param state [String] The state token to bind
|
126
|
+
# @return [String] The bound state token (state::hmac format)
|
127
|
+
def bind_state_to_session(controller, state)
|
128
|
+
session_id = controller.request.session.id
|
129
|
+
hmac = OpenSSL::HMAC.hexdigest("SHA256", session_id, state)
|
130
|
+
"#{state}#{STATE_HMAC_DELIMITER}#{hmac}"
|
131
|
+
end
|
132
|
+
|
133
|
+
# Validates a state token that was bound to the session
|
134
|
+
# @param controller [ActionController::Base] The controller instance
|
135
|
+
# @param bound_state [String] The bound state token (state::hmac format)
|
136
|
+
# @return [String] The original state if valid
|
137
|
+
# @raise [Clavis::InvalidState] If the state is invalid or HMAC doesn't match
|
138
|
+
def validate_bound_state(controller, bound_state)
|
139
|
+
# Split using the delimiter - allows state to contain hyphens
|
140
|
+
parts = bound_state.to_s.split(STATE_HMAC_DELIMITER)
|
141
|
+
|
142
|
+
# We expect exactly 2 parts: state and hmac
|
143
|
+
raise Clavis::InvalidState if parts.length != 2
|
144
|
+
|
145
|
+
state = parts[0]
|
146
|
+
received_hmac = parts[1]
|
147
|
+
|
148
|
+
# Basic validation
|
149
|
+
raise Clavis::InvalidState if state.nil? || received_hmac.nil? || state.empty? || received_hmac.empty?
|
150
|
+
|
151
|
+
# Verify HMAC
|
152
|
+
session_id = controller.request.session.id
|
153
|
+
expected_hmac = OpenSSL::HMAC.hexdigest("SHA256", session_id, state)
|
154
|
+
|
155
|
+
raise Clavis::InvalidState unless received_hmac == expected_hmac
|
156
|
+
|
157
|
+
# Return the original state if valid
|
158
|
+
state
|
75
159
|
end
|
76
160
|
end
|
77
161
|
end
|
@@ -68,6 +68,16 @@ module Clavis
|
|
68
68
|
nonce
|
69
69
|
end
|
70
70
|
|
71
|
+
# Retrieve the stored nonce from the session
|
72
|
+
# @param session [Hash] The session hash
|
73
|
+
# @param clear_after_retrieval [Boolean] Whether to clear the nonce after retrieval
|
74
|
+
# @return [String, nil] The stored nonce or nil if not found
|
75
|
+
def retrieve_nonce(session, clear_after_retrieval: false)
|
76
|
+
nonce = retrieve(session, :oauth_nonce)
|
77
|
+
delete(session, :oauth_nonce) if clear_after_retrieval
|
78
|
+
nonce
|
79
|
+
end
|
80
|
+
|
71
81
|
# Check if a nonce is valid
|
72
82
|
# @param session [Hash] The session hash
|
73
83
|
# @param nonce [String] The nonce to validate
|
data/lib/clavis/version.rb
CHANGED
data/lib/clavis.rb
CHANGED
@@ -16,6 +16,11 @@ require_relative "clavis/security/session_manager"
|
|
16
16
|
require_relative "clavis/security/rate_limiter"
|
17
17
|
require "clavis/user_info_normalizer"
|
18
18
|
|
19
|
+
# Load required gems
|
20
|
+
require "jwt"
|
21
|
+
require "json"
|
22
|
+
require "faraday"
|
23
|
+
|
19
24
|
# Only load provider classes if they're not already defined (for testing)
|
20
25
|
unless defined?(Clavis::Providers::Base)
|
21
26
|
require_relative "clavis/providers/base"
|
@@ -103,32 +103,49 @@ module Clavis
|
|
103
103
|
end
|
104
104
|
|
105
105
|
def show_post_install_message
|
106
|
-
say "\nClavis has been installed successfully!"
|
107
|
-
|
108
|
-
#
|
109
|
-
say "\
|
110
|
-
|
106
|
+
say "\nClavis has been installed successfully! š"
|
107
|
+
|
108
|
+
# What was done section
|
109
|
+
say "\n=== What Was Done ==="
|
110
|
+
say "ā
Generated migration for OAuth identities"
|
111
|
+
say "ā
Added OAuth fields to your User model"
|
112
|
+
say "ā
Created ClavisUserMethods concern for your User model"
|
113
|
+
say "ā
Mounted Clavis engine at '/auth' in routes.rb"
|
114
|
+
say "ā
Generated configuration initializer"
|
115
|
+
|
116
|
+
# Required steps section
|
117
|
+
say "\n=== Required Steps ==="
|
111
118
|
steps = []
|
112
119
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
steps << "Configure your providers in config/initializers/clavis.rb"
|
118
|
-
steps << "Run migrations: rails db:migrate"
|
119
|
-
steps << "ā ļø Customize the user creation code in app/models/concerns/clavis_user_methods.rb"
|
120
|
-
steps << "Add OAuth buttons to your views:\n <%= clavis_oauth_button :google %>"
|
120
|
+
steps << "Run migrations:\n $ rails db:migrate"
|
121
|
+
steps << "Configure your providers in config/initializers/clavis.rb:\n ā¢ Add your client_id and client_secret\n ā¢ Set correct redirect_uri values" # rubocop:disable Layout/LineLength
|
122
|
+
steps << "ā ļø IMPORTANT: Customize user creation in app/models/concerns/clavis_user_methods.rb\n ā¢ The default only sets the email field, which is likely insufficient\n ā¢ Add all required fields for your User model" # rubocop:disable Layout/LineLength
|
121
123
|
|
122
124
|
# Output numbered steps
|
123
125
|
steps.each_with_index do |step, index|
|
124
126
|
say "#{index + 1}. #{step}"
|
125
127
|
end
|
126
128
|
|
127
|
-
|
128
|
-
say "
|
129
|
-
say "
|
129
|
+
# Password validation section
|
130
|
+
say "\n=== For Password-Protected Users ==="
|
131
|
+
say "If your User model uses has_secure_password:"
|
132
|
+
say "ā¢ Uncomment the password validation section in app/models/concerns/clavis_user_methods.rb"
|
133
|
+
say "ā¢ Choose one of the approaches described there"
|
134
|
+
|
135
|
+
# View integration section
|
136
|
+
say "\n=== Using In Your Views ==="
|
137
|
+
say "Add OAuth buttons to your login page:"
|
138
|
+
say "<%= clavis_oauth_button :google %>"
|
139
|
+
say "<%= clavis_oauth_button :github %>"
|
140
|
+
|
141
|
+
# CSS styling section
|
142
|
+
if @provide_css_instructions
|
143
|
+
say "\n=== For CSS Styling ==="
|
144
|
+
say "Include Clavis styles in your layout:"
|
145
|
+
say "<%= stylesheet_link_tag 'clavis_styles' %>"
|
146
|
+
end
|
130
147
|
|
131
|
-
say "\nFor more information, see
|
148
|
+
say "\nFor more information, see: https://github.com/clayton/clavis"
|
132
149
|
end
|
133
150
|
|
134
151
|
private
|
@@ -17,8 +17,10 @@ Clavis.configure do |config|
|
|
17
17
|
# Default scopes to request from providers
|
18
18
|
# config.default_scopes = "email profile"
|
19
19
|
|
20
|
-
# Enable verbose logging for
|
21
|
-
#
|
20
|
+
# Enable verbose logging for authentication processes (disabled by default)
|
21
|
+
# When enabled, this will log details about token exchanges, user info requests,
|
22
|
+
# token refreshes, etc. for debugging purposes
|
23
|
+
# config.verbose_logging = false
|
22
24
|
|
23
25
|
# User class and finder method
|
24
26
|
# These settings control how Clavis finds or creates users from OAuth data
|
@@ -197,22 +197,11 @@ module Clavis
|
|
197
197
|
end
|
198
198
|
|
199
199
|
def show_instructions
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
say "
|
204
|
-
say "
|
205
|
-
|
206
|
-
say "\nā ļø IMPORTANT: If your User model uses has_secure_password, you need to handle password validation!"
|
207
|
-
say "Look for the password validation section in app/models/concerns/clavis_user_methods.rb and"
|
208
|
-
say "uncomment one of the approaches described there."
|
209
|
-
|
210
|
-
say "\nTo customize:"
|
211
|
-
say " 1. Edit app/models/concerns/clavis_user_methods.rb"
|
212
|
-
say " 2. Add required fields to the user creation in find_or_create_from_clavis"
|
213
|
-
say " 3. Handle password validation if your model uses has_secure_password"
|
214
|
-
|
215
|
-
say "\nFor more information, see the documentation at https://github.com/clayton/clavis"
|
200
|
+
# The main instructions will be handled by install_generator.rb
|
201
|
+
# This is just a simple confirmation of what was done
|
202
|
+
say "\nClavis user methods have been added to your User model."
|
203
|
+
say "ā
Created app/models/concerns/clavis_user_methods.rb"
|
204
|
+
say "ā
Added 'include ClavisUserMethods' to your User model"
|
216
205
|
end
|
217
206
|
end
|
218
207
|
end
|