Class: Capsium::Reactor::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/capsium/reactor/session.rb,
sig/capsium/reactor/session.rbs

Overview

HMAC-SHA256 signed session cookies (05x-authentication). The signing secret comes from deploy.json (sessionSecret) or is generated and persisted reactor-side (never in the package).

Constant Summary collapse

"capsium_session"
SECRET_BYTES =

Returns:

  • (Integer)
32

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret: nil, state_file: nil) ⇒ Session

Returns a new instance of Session.

Parameters:

  • secret: (String? secret) (defaults to: nil)
  • state_file: (String? state_file) (defaults to: nil)


26
27
28
# File 'lib/capsium/reactor/session.rb', line 26

def initialize(secret: nil, state_file: nil)
  @secret = secret || self.class.load_or_generate_secret(state_file)
end

Instance Attribute Details

#secretString (readonly)

Returns the value of attribute secret.

Returns:

  • (String)


24
25
26
# File 'lib/capsium/reactor/session.rb', line 24

def secret
  @secret
end

Class Method Details

.hmac(secret, data) ⇒ String

Parameters:

  • secret (String)
  • data (String)

Returns:

  • (String)


20
21
22
# File 'lib/capsium/reactor/session.rb', line 20

def self.hmac(secret, data)
  OpenSSL::HMAC.hexdigest("SHA256", secret, data)
end

.load_or_generate_secret(state_file) ⇒ String

The persisted reactor-side secret: loaded from state_file when present, generated and written (mode 0600) otherwise.

Parameters:

  • state_file (String, nil)

Returns:

  • (String)


37
38
39
40
41
42
43
44
45
# File 'lib/capsium/reactor/session.rb', line 37

def self.load_or_generate_secret(state_file)
  raise Error, "session secret or state_file is required" unless state_file

  return File.read(state_file).strip if File.file?(state_file)

  secret = SecureRandom.hex(SECRET_BYTES)
  File.write(state_file, secret, perm: 0o600)
  secret
end

Instance Method Details

A Set-Cookie value establishing the identity session.

Parameters:

  • identity (Hash[String, untyped])

Returns:

  • (String)


77
78
79
# File 'lib/capsium/reactor/session.rb', line 77

def cookie_for(identity)
  "#{COOKIE_NAME}=#{encode(identity)}; Path=/; HttpOnly; SameSite=Lax"
end

#decode(cookie_value) ⇒ Hash[String, untyped]?

The verified payload, or nil when the cookie is missing or the signature does not match.

Parameters:

  • cookie_value (String, nil)

Returns:

  • (Hash[String, untyped], nil)


54
55
56
57
58
59
60
61
62
63
64
# File 'lib/capsium/reactor/session.rb', line 54

def decode(cookie_value)
  data, signature = cookie_value.to_s.split(".", 2)
  return if data.nil? || signature.nil?

  expected = self.class.hmac(@secret, data)
  return unless Reactor.secure_compare(expected, signature)

  JSON.parse(Base64.urlsafe_decode64(data))
rescue JSON::ParserError, ArgumentError
  nil
end

#encode(payload) ⇒ String

Parameters:

  • payload (Hash[String, untyped])

Returns:

  • (String)


47
48
49
50
# File 'lib/capsium/reactor/session.rb', line 47

def encode(payload)
  data = Base64.urlsafe_encode64(JSON.generate(payload), padding: false)
  "#{data}.#{self.class.hmac(@secret, data)}"
end

#identity_from(request) ⇒ Hash[String, untyped]?

The identity for a request's Cookie header, or nil.

Parameters:

  • request (Object)

Returns:

  • (Hash[String, untyped], nil)


67
68
69
70
71
72
73
74
# File 'lib/capsium/reactor/session.rb', line 67

def identity_from(request)
  cookie = request["Cookie"].to_s.split(/;\s*/).find do |part|
    part.start_with?("#{COOKIE_NAME}=")
  end
  return unless cookie

  decode(cookie.delete_prefix("#{COOKIE_NAME}="))
end

#sign(data) ⇒ String

HMAC-SHA256 of data with the session secret (hex).

Parameters:

  • data (String)

Returns:

  • (String)


31
32
33
# File 'lib/capsium/reactor/session.rb', line 31

def sign(data)
  self.class.hmac(@secret, data)
end