# frozen_string_literal: true
require "rack/session/cookie"
require_relative "utils"
class Tynn
# Adds simple cookie based session management. You can pass a secret
# token to sign the cookie data, thus unauthorized means can't alter it.
#
# require "tynn"
# require "tynn/session"
#
# Tynn.plugin(Tynn::Session, secret: "__change_me_not_secure__")
#
# Tynn.define do
# on "login" do
# on post do
# # ...
#
# session[:user_id] = user.id
#
# res.redirect("/admin")
# end
# end
# end
#
# The following command generates a cryptographically secure secret ready
# to use:
#
# $ ruby -r securerandom -e "puts SecureRandom.hex(64)"
#
# It's important to keep the token secret. Knowing the token allows an
# attacker to tamper the data. So, it's recommended to load the token
# from the environment.
#
# Tynn.plugin(Tynn::Session, secret: ENV["SESSION_SECRET"])
#
# Under the hood, Tynn::Session uses the Rack::Session::Cookie
# middleware. Thus, supports all the options available for this middleware:
#
# [key]
# The name of the cookie. Defaults to "rack.session".
#
# [httponly]
# If true, sets the HttpOnly flag. This mitigates the
# risk of client side scripting accessing the cookie. Defaults to true.
#
# [secure]
# If true, sets the Secure flag. This tells the browser
# to only transmit the cookie over HTTPS. Defaults to false.
#
# [same_site]
# Disables third-party usage for cookies. There are two possible values
# :Lax and :Strict. In Strict mode, the cookie
# is restrain to any cross-site usage; in Lax mode, some cross-site
# usage is allowed. Defaults to :Lax. If nil is passed,
# the flag is not included. Check this article[http://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/]
# for more information.
#
# [expire_after]
# The lifespan of the cookie. If nil, the session cookie is temporary
# and is no retained after the browser is closed. Defaults to nil.
#
#
#
# Tynn.plugin(
# Tynn::Session,
# key: "app",
# secret: ENV["SESSION_SECRET"],
# expire_after: 36_000, # seconds
# httponly: true,
# secure: true,
# same_site: :Strict
# )
#
module Session
SECRET_MIN_LENGTH = 30 # :nodoc:
def self.setup(app, options = {}) # :nodoc:
secret = options[:secret]
if secret.nil?
Tynn::Utils.raise_error(
"Secret key is required",
error: ArgumentError,
tag: :no_secret_key
)
end
if secret.length < SECRET_MIN_LENGTH
Tynn::Utils.raise_error(
"Secret key is shorter than #{ SECRET_MIN_LENGTH } characters",
error: ArgumentError,
tag: :short_secret_key
)
end
app.use(Rack::Session::Cookie, {
coder: Rack::Session::Cookie::Base64::JSON.new,
hmac: OpenSSL::Digest::SHA256,
same_site: :Lax
}.merge(options))
end
module InstanceMethods
# Returns the session hash.
#
# session
# # => {}
#
# session[:foo] = "foo"
# session[:foo]
# # => "foo"
#
def session
req.session
end
end
end
end