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.
@@ -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(24)
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(16)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Clavis
4
4
  # The current version of Clavis.
5
- VERSION = "0.7.1"
5
+ VERSION = "0.8.0"
6
6
  end
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
- # Next steps
109
- say "\nNext steps:"
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
- if @provide_css_instructions
114
- steps << "Include the Clavis styles in your layout:\n <%= stylesheet_link_tag 'clavis_styles' %>"
115
- end
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
- say "\nClavis has configured your User model with OAuth support via the ClavisUserMethods concern."
128
- say "IMPORTANT: The default implementation only sets the email field when creating users."
129
- say "You MUST customize this to include all required fields for your User model."
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 the documentation at https://github.com/clayton/clavis"
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 debugging
21
- # config.verbose_logging = true
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
- say "\nThe ClavisUserMethods concern has been created and included in your User model."
201
- say "This gives your User model the ability to find or create users from OAuth data."
202
-
203
- say "\nāš ļø IMPORTANT: You must customize the user creation code to match your User model!"
204
- say "The default implementation only sets the email field, which may not be sufficient."
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