Class: Capsium::Reactor::Session
- Inherits:
-
Object
- Object
- Capsium::Reactor::Session
- 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
- COOKIE_NAME =
"capsium_session"- SECRET_BYTES =
32
Instance Attribute Summary collapse
-
#secret ⇒ String
readonly
Returns the value of attribute secret.
Class Method Summary collapse
- .hmac(secret, data) ⇒ String
-
.load_or_generate_secret(state_file) ⇒ String
The persisted reactor-side secret: loaded from state_file when present, generated and written (mode 0600) otherwise.
Instance Method Summary collapse
-
#cookie_for(identity) ⇒ String
A Set-Cookie value establishing the identity session.
-
#decode(cookie_value) ⇒ Hash[String, untyped]?
The verified payload, or nil when the cookie is missing or the signature does not match.
- #encode(payload) ⇒ String
-
#identity_from(request) ⇒ Hash[String, untyped]?
The identity for a request's Cookie header, or nil.
-
#initialize(secret: nil, state_file: nil) ⇒ Session
constructor
A new instance of Session.
-
#sign(data) ⇒ String
HMAC-SHA256 of data with the session secret (hex).
Constructor Details
#initialize(secret: nil, state_file: nil) ⇒ Session
Returns a new instance of Session.
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
#secret ⇒ String (readonly)
Returns the value of attribute secret.
24 25 26 |
# File 'lib/capsium/reactor/session.rb', line 24 def secret @secret end |
Class Method Details
.hmac(secret, data) ⇒ 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.
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
#cookie_for(identity) ⇒ String
A Set-Cookie value establishing the identity session.
77 78 79 |
# File 'lib/capsium/reactor/session.rb', line 77 def (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.
54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/capsium/reactor/session.rb', line 54 def decode() data, signature = .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
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.
67 68 69 70 71 72 73 74 |
# File 'lib/capsium/reactor/session.rb', line 67 def identity_from(request) = request["Cookie"].to_s.split(/;\s*/).find do |part| part.start_with?("#{COOKIE_NAME}=") end return unless decode(.delete_prefix("#{COOKIE_NAME}=")) end |
#sign(data) ⇒ String
HMAC-SHA256 of data with the session secret (hex).
31 32 33 |
# File 'lib/capsium/reactor/session.rb', line 31 def sign(data) self.class.hmac(@secret, data) end |