clavis 0.7.2 → 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/README.md +105 -0
- 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/templates/initializer.rb +4 -2
- metadata +3 -2
@@ -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"
|
@@ -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
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clavis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clayton Lengel-Zigich
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-21 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: faraday
|
@@ -197,6 +197,7 @@ files:
|
|
197
197
|
- lib/clavis/providers/github.rb
|
198
198
|
- lib/clavis/providers/google.rb
|
199
199
|
- lib/clavis/providers/microsoft.rb
|
200
|
+
- lib/clavis/providers/token_exchange_handler.rb
|
200
201
|
- lib/clavis/security/csrf_protection.rb
|
201
202
|
- lib/clavis/security/https_enforcer.rb
|
202
203
|
- lib/clavis/security/input_validator.rb
|