Module: Clavis::Security::CsrfProtection

Defined in:
lib/clavis/security/csrf_protection.rb

Constant Summary collapse

STATE_HMAC_DELIMITER =

Delimiter used to separate state from HMAC

"::"

Class Method Summary collapse

Class Method Details

.bind_state_to_session(controller, state) ⇒ String

Binds a state token to the session context for extra security

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • state (String)

    The state token to bind

Returns:

  • (String)

    The bound state token (state::hmac format)



127
128
129
130
131
# File 'lib/clavis/security/csrf_protection.rb', line 127

def bind_state_to_session(controller, state)
  session_id = controller.request.session.id
  hmac = OpenSSL::HMAC.hexdigest("SHA256", session_id, state)
  "#{state}#{STATE_HMAC_DELIMITER}#{hmac}"
end

.generate_nonce(length = 16) ⇒ String

Generates a nonce for OIDC requests

Parameters:

  • length (Integer) (defaults to: 16)

    The byte length for the nonce (resulting hex string will be twice this length)

Returns:

  • (String)

    A secure random nonce



75
76
77
# File 'lib/clavis/security/csrf_protection.rb', line 75

def generate_nonce(length = 16)
  SecureRandom.hex(length)
end

.generate_state(length = 24) ⇒ String

Generates a secure random state token for CSRF protection

Parameters:

  • length (Integer) (defaults to: 24)

    The byte length for the token (resulting hex string will be twice this length)

Returns:

  • (String)

    A secure random state token



15
16
17
# File 'lib/clavis/security/csrf_protection.rb', line 15

def generate_state(length = 24)
  SecureRandom.hex(length)
end

.store_nonce_in_session(controller, expiry = nil, length = 16) ⇒ String

Stores a nonce in the Rails session

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • expiry (Time, Integer) (defaults to: nil)

    Optional expiration time (Time object or seconds from now)

  • length (Integer) (defaults to: 16)

    Optional byte length for the nonce

Returns:

  • (String)

    The generated nonce



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/clavis/security/csrf_protection.rb', line 84

def store_nonce_in_session(controller, expiry = nil, length = 16)
  nonce = generate_nonce(length)
  controller.session[:oauth_nonce] = nonce

  # Store expiration if provided
  if expiry
    expiry_time = expiry.is_a?(Integer) ? Time.now.to_i + expiry : expiry.to_i
    controller.session[:oauth_nonce_expiry] = expiry_time
  end

  nonce
end

.store_state_in_session(controller, expiry = nil, length = 24) ⇒ String

Stores a state token in the Rails session

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • expiry (Time, Integer) (defaults to: nil)

    Optional expiration time (Time object or seconds from now)

  • length (Integer) (defaults to: 24)

    Optional byte length for the token

Returns:

  • (String)

    The generated state token



34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/clavis/security/csrf_protection.rb', line 34

def store_state_in_session(controller, expiry = nil, length = 24)
  state = generate_state(length)
  controller.session[:oauth_state] = state

  # Store expiration if provided
  if expiry
    expiry_time = expiry.is_a?(Integer) ? Time.now.to_i + expiry : expiry.to_i
    controller.session[:oauth_state_expiry] = expiry_time
  end

  state
end

.validate_bound_state(controller, bound_state) ⇒ String

Validates a state token that was bound to the session

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • bound_state (String)

    The bound state token (state::hmac format)

Returns:

  • (String)

    The original state if valid

Raises:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/clavis/security/csrf_protection.rb', line 138

def validate_bound_state(controller, bound_state)
  # Split using the delimiter - allows state to contain hyphens
  parts = bound_state.to_s.split(STATE_HMAC_DELIMITER)

  # We expect exactly 2 parts: state and hmac
  raise Clavis::InvalidState if parts.length != 2

  state = parts[0]
  received_hmac = parts[1]

  # Basic validation
  raise Clavis::InvalidState if state.nil? || received_hmac.nil? || state.empty? || received_hmac.empty?

  # Verify HMAC
  session_id = controller.request.session.id
  expected_hmac = OpenSSL::HMAC.hexdigest("SHA256", session_id, state)

  raise Clavis::InvalidState unless received_hmac == expected_hmac

  # Return the original state if valid
  state
end

.validate_nonce_from_session!(controller, id_token_nonce) ⇒ Object

Validates the nonce from the ID token against the one in the session

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • id_token_nonce (String)

    The nonce from the ID token

Raises:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/clavis/security/csrf_protection.rb', line 103

def validate_nonce_from_session!(controller, id_token_nonce)
  expected_nonce = controller.session[:oauth_nonce]
  expiry = controller.session[:oauth_nonce_expiry]

  # Check for expiration if an expiry was set
  if expiry && Time.now.to_i > expiry
    # Clear expired nonce from session
    controller.session.delete(:oauth_nonce)
    controller.session.delete(:oauth_nonce_expiry)
    raise Clavis::ExpiredState
  end

  raise Clavis::MissingNonce if id_token_nonce.nil? || expected_nonce.nil?
  raise Clavis::InvalidNonce unless id_token_nonce == expected_nonce

  # Clear the nonce from the session after validation
  controller.session.delete(:oauth_nonce)
  controller.session.delete(:oauth_nonce_expiry)
end

.validate_state!(actual_state, expected_state) ⇒ Object

Validates that the actual state matches the expected state

Parameters:

  • actual_state (String)

    The state received from the OAuth provider

  • expected_state (String)

    The state that was originally sent

Raises:



24
25
26
27
# File 'lib/clavis/security/csrf_protection.rb', line 24

def validate_state!(actual_state, expected_state)
  raise Clavis::MissingState if actual_state.nil? || expected_state.nil?
  raise Clavis::InvalidState unless actual_state == expected_state
end

.validate_state_from_session!(controller, actual_state) ⇒ Object

Validates the state from the Rails session

Parameters:

  • controller (ActionController::Base)

    The controller instance

  • actual_state (String)

    The state received from the OAuth provider

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/clavis/security/csrf_protection.rb', line 53

def validate_state_from_session!(controller, actual_state)
  expected_state = controller.session[:oauth_state]
  expiry = controller.session[:oauth_state_expiry]

  # Check for expiration if an expiry was set
  if expiry && Time.now.to_i > expiry
    # Clear expired state from session
    controller.session.delete(:oauth_state)
    controller.session.delete(:oauth_state_expiry)
    raise Clavis::ExpiredState
  end

  validate_state!(actual_state, expected_state)

  # Clear the state from the session after validation
  controller.session.delete(:oauth_state)
  controller.session.delete(:oauth_state_expiry)
end