parent
25369e7e27
commit
00ee70f5a9
@ -0,0 +1,223 @@ |
|||||||
|
defmodule BlockScoutWeb.Plug.RedisCookie do |
||||||
|
@moduledoc """ |
||||||
|
Extended version of Plug.Session.COOKIE from https://github.com/elixir-plug/plug/blob/main/lib/plug/session/cookie.ex |
||||||
|
Added Redis to have a possibility to invalidate session |
||||||
|
""" |
||||||
|
|
||||||
|
require Logger |
||||||
|
@behaviour Plug.Session.Store |
||||||
|
|
||||||
|
alias Plug.Crypto |
||||||
|
alias Plug.Crypto.{KeyGenerator, MessageEncryptor, MessageVerifier} |
||||||
|
|
||||||
|
@impl true |
||||||
|
def init(opts) do |
||||||
|
opts |
||||||
|
|> build_opts() |
||||||
|
|> build_rotating_opts(opts[:rotating_options]) |
||||||
|
|> Map.delete(:secret_key_base) |
||||||
|
end |
||||||
|
|
||||||
|
@impl true |
||||||
|
def get(conn, raw_cookie, opts) do |
||||||
|
opts = Map.put(opts, :secret_key_base, conn.secret_key_base) |
||||||
|
|
||||||
|
[opts | opts.rotating_options] |
||||||
|
|> Enum.find_value(:error, &read_raw_cookie(raw_cookie, &1)) |
||||||
|
|> decode(opts.serializer, opts.log) |
||||||
|
|> check_in_redis(raw_cookie) |
||||||
|
end |
||||||
|
|
||||||
|
@impl true |
||||||
|
def put(conn, _sid, term, opts) do |
||||||
|
%{serializer: serializer, key_opts: key_opts, signing_salt: signing_salt} = opts |
||||||
|
binary = encode(term, serializer) |
||||||
|
|
||||||
|
opts |
||||||
|
|> case do |
||||||
|
%{encryption_salt: nil} -> |
||||||
|
MessageVerifier.sign(binary, derive(conn.secret_key_base, signing_salt, key_opts)) |
||||||
|
|
||||||
|
%{encryption_salt: encryption_salt} -> |
||||||
|
MessageEncryptor.encrypt( |
||||||
|
binary, |
||||||
|
derive(conn.secret_key_base, encryption_salt, key_opts), |
||||||
|
derive(conn.secret_key_base, signing_salt, key_opts) |
||||||
|
) |
||||||
|
end |
||||||
|
|> store_to_redis() |
||||||
|
end |
||||||
|
|
||||||
|
@impl true |
||||||
|
def delete(_conn, sid, _opts) do |
||||||
|
remove_from_redis(sid) |
||||||
|
:ok |
||||||
|
end |
||||||
|
|
||||||
|
defp encode(term, :external_term_format) do |
||||||
|
:erlang.term_to_binary(term) |
||||||
|
end |
||||||
|
|
||||||
|
defp encode(term, serializer) do |
||||||
|
{:ok, binary} = serializer.encode(term) |
||||||
|
binary |
||||||
|
end |
||||||
|
|
||||||
|
defp decode({:ok, binary}, :external_term_format, log) do |
||||||
|
{:term, |
||||||
|
try do |
||||||
|
Crypto.non_executable_binary_to_term(binary) |
||||||
|
rescue |
||||||
|
e -> |
||||||
|
Logger.log( |
||||||
|
log, |
||||||
|
"Plug.Session could not decode incoming session cookie. Reason: " <> |
||||||
|
Exception.message(e) |
||||||
|
) |
||||||
|
|
||||||
|
%{} |
||||||
|
end} |
||||||
|
end |
||||||
|
|
||||||
|
defp decode({:ok, binary}, serializer, _log) do |
||||||
|
case serializer.decode(binary) do |
||||||
|
{:ok, term} -> {:custom, term} |
||||||
|
_ -> {:custom, %{}} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp decode(:error, _serializer, false) do |
||||||
|
{nil, %{}} |
||||||
|
end |
||||||
|
|
||||||
|
defp decode(:error, _serializer, log) do |
||||||
|
Logger.log( |
||||||
|
log, |
||||||
|
"Plug.Session could not verify incoming session cookie. " <> |
||||||
|
"This may happen when the session settings change or a stale cookie is sent." |
||||||
|
) |
||||||
|
|
||||||
|
{nil, %{}} |
||||||
|
end |
||||||
|
|
||||||
|
defp prederive(secret_key_base, value, key_opts) |
||||||
|
when is_binary(secret_key_base) and is_binary(value) do |
||||||
|
{:prederived, derive(secret_key_base, value, Keyword.delete(key_opts, :cache))} |
||||||
|
end |
||||||
|
|
||||||
|
defp prederive(_secret_key_base, value, _key_opts) do |
||||||
|
value |
||||||
|
end |
||||||
|
|
||||||
|
defp derive(_secret_key_base, {:prederived, value}, _key_opts) do |
||||||
|
value |
||||||
|
end |
||||||
|
|
||||||
|
defp derive(secret_key_base, {module, function, args}, key_opts) do |
||||||
|
derive(secret_key_base, apply(module, function, args), key_opts) |
||||||
|
end |
||||||
|
|
||||||
|
defp derive(secret_key_base, key, key_opts) do |
||||||
|
secret_key_base |
||||||
|
|> validate_secret_key_base() |
||||||
|
|> KeyGenerator.generate(key, key_opts) |
||||||
|
end |
||||||
|
|
||||||
|
defp validate_secret_key_base(nil), |
||||||
|
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be set") |
||||||
|
|
||||||
|
defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64, |
||||||
|
do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be at least 64 bytes") |
||||||
|
|
||||||
|
defp validate_secret_key_base(secret_key_base), do: secret_key_base |
||||||
|
|
||||||
|
defp check_signing_salt(opts) do |
||||||
|
case opts[:signing_salt] do |
||||||
|
nil -> raise ArgumentError, "cookie store expects :signing_salt as option" |
||||||
|
salt -> salt |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp check_serializer(serializer) when is_atom(serializer), do: serializer |
||||||
|
|
||||||
|
defp check_serializer(_), |
||||||
|
do: raise(ArgumentError, "cookie store expects :serializer option to be a module") |
||||||
|
|
||||||
|
defp read_raw_cookie(raw_cookie, opts) do |
||||||
|
signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts) |
||||||
|
|
||||||
|
opts |
||||||
|
|> case do |
||||||
|
%{encryption_salt: nil} -> |
||||||
|
MessageVerifier.verify(raw_cookie, signing_salt) |
||||||
|
|
||||||
|
%{encryption_salt: _} -> |
||||||
|
encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts) |
||||||
|
|
||||||
|
MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt) |
||||||
|
end |
||||||
|
|> case do |
||||||
|
:error -> nil |
||||||
|
result -> result |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp build_opts(opts) do |
||||||
|
encryption_salt = opts[:encryption_salt] |
||||||
|
signing_salt = check_signing_salt(opts) |
||||||
|
|
||||||
|
iterations = Keyword.get(opts, :key_iterations, 1000) |
||||||
|
length = Keyword.get(opts, :key_length, 32) |
||||||
|
digest = Keyword.get(opts, :key_digest, :sha256) |
||||||
|
log = Keyword.get(opts, :log, :debug) |
||||||
|
secret_key_base = Keyword.get(opts, :secret_key_base) |
||||||
|
key_opts = [iterations: iterations, length: length, digest: digest, cache: Plug.Keys] |
||||||
|
|
||||||
|
serializer = check_serializer(opts[:serializer] || :external_term_format) |
||||||
|
|
||||||
|
%{ |
||||||
|
secret_key_base: secret_key_base, |
||||||
|
encryption_salt: prederive(secret_key_base, encryption_salt, key_opts), |
||||||
|
signing_salt: prederive(secret_key_base, signing_salt, key_opts), |
||||||
|
key_opts: key_opts, |
||||||
|
serializer: serializer, |
||||||
|
log: log |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
defp build_rotating_opts(opts, rotating_opts) when is_list(rotating_opts) do |
||||||
|
Map.put(opts, :rotating_options, Enum.map(rotating_opts, &build_opts/1)) |
||||||
|
end |
||||||
|
|
||||||
|
defp build_rotating_opts(opts, _), do: Map.put(opts, :rotating_options, []) |
||||||
|
|
||||||
|
defp store_to_redis(cookie) do |
||||||
|
Redix.command(:redix, ["SET", hash(cookie), 1]) |
||||||
|
|
||||||
|
cookie |
||||||
|
end |
||||||
|
|
||||||
|
defp remove_from_redis(sid) do |
||||||
|
Redix.command(:redix, ["DEL", sid]) |
||||||
|
end |
||||||
|
|
||||||
|
defp check_in_redis({sid, map}, _cookie) when is_nil(sid) or map == %{}, do: {nil, %{}} |
||||||
|
|
||||||
|
defp check_in_redis({_sid, session}, cookie) do |
||||||
|
hash = hash(cookie) |
||||||
|
|
||||||
|
case Redix.command(:redix, ["GET", hash]) do |
||||||
|
{:ok, one} when one in [1, "1"] -> |
||||||
|
{hash, session} |
||||||
|
|
||||||
|
_ -> |
||||||
|
{nil, %{}} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp hash(cookie) do |
||||||
|
:sha256 |
||||||
|
|> :crypto.hash(cookie) |
||||||
|
|> Base.encode16() |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue