Packages
raxx
0.17.4
1.1.0
1.0.1
1.0.0
1.0.0-rc.3
1.0.0-rc.2
retired
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.18.1
0.18.0
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
retired
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.14
0.14.13
0.14.12
0.14.11
0.14.10
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.1
0.11.0
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
0.0.1
Interface for HTTP webservers, frameworks and clients.
Current section
Files
Jump to
Current section
Files
lib/raxx/session/signed_cookie.ex
defmodule Raxx.Session.SignedCookie do
@moduledoc """
Use signed cookies to store the session for a client.
Sessions stored this way are signed to ensure that they have not been tampered.
The secret given to config must be kept secure to prevent sessions being tampered
**NOTE:** the session is not encrypted so a user can read any value from the session, they are just unable to modify it.
### Configuring sessions
Configuration is required to use signed cookies a session store.
The most important value is the secret that is used when signing and verifying.
To set up an new configuration use `config/1`.
It often makes sense to keep your session config in the application state
def handle_request(request, %{sessions: session_config})
### Working with sessions
A session can be any term.
Use `embed/3` to set a new session value for a client.
Embedding a new session value will override any previouse value.
iex> config = SignedCookie.config(secret: "eggplant")
...> Raxx.response(:no_content)
...> |> SignedCookie.embed({:user, 25}, config)
...> |> Raxx.get_header("set-cookie")
"raxx.session=g2gCZAAEdXNlcmEZ--gpW5K8Pgle9isXR5Qymz4m2VEU1DuEosNfgpLTQuRn0=; path=/; HttpOnly"
A client that has received a session should send it with all subequent requests.
A session can be retrieved using `extract/2`.
This step will also verify that the session has not been tampered with.
iex> config = SignedCookie.config(secret: "eggplant")
...> Raxx.request(:GET, "/")
...> |> Raxx.set_header("cookie", "raxx.session=g2gCZAAEdXNlcmEZ--gpW5K8Pgle9isXR5Qymz4m2VEU1DuEosNfgpLTQuRn0=")
...> |> SignedCookie.extract(config)
{:ok, {:user, 25}}
iex> config = SignedCookie.config(secret: "eggplant")
...> Raxx.request(:GET, "/")
...> |> Raxx.set_header("cookie", "raxx.session=g2gCZAAEdXNlcmEZ--sfbxaB-IEgUt_NwdmmZpJny9OzOx15D-6uwusW6X1ZY=")
...> |> SignedCookie.extract(config)
{:error, :could_not_verify_signature}
A session can be concluded by marking as expired.
For example in response to a users sign out request.
iex> config = SignedCookie.config(secret: "eggplant")
...> Raxx.response(:no_content)
...> |> SignedCookie.expire(config)
...> |> Raxx.get_header("set-cookie")
"raxx.session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; HttpOnly"
## NOTE
- This module will be extracted from the Raxx project before 1.0 release.
- The Rack.Session.Cookie module was the inspiration for this functionality.
https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb
"""
@default_cookie_name "raxx.session"
@enforce_keys [:secret, :cookie_name, :previous_secrets]
defstruct @enforce_keys
@doc """
Setup configuration to work with sessions
## Options
- **secret:** (required) a secure random value used for signing session data.
- **cookie_name:** default is `"raxx.session"`
- **previous_secrets:** acceptable secrets to verify against old sessions.
"""
def config(options) do
secret =
case Keyword.fetch(options, :secret) do
{:ok, secret} ->
secret
:error ->
raise "A `:secret` must be set when using signed cookies."
end
cookie_name = Keyword.get(options, :cookie_name, @default_cookie_name)
previous_secrets = Keyword.get(options, :previous_secrets, [])
%__MODULE__{
secret: secret,
cookie_name: cookie_name,
previous_secrets: previous_secrets
}
end
@doc """
Overwrite a clients session with a new value.
"""
def embed(response, session, session_config = %__MODULE__{}) do
payload = safe_encode(session)
digest = safe_digest(payload, session_config.secret)
# Where to put 4096 check
# It could be in Raxx.set_cookie
response
|> Raxx.set_header(
"set-cookie",
SetCookie.serialize(session_config.cookie_name, payload <> "--" <> digest)
)
end
@doc """
Extract and verify the session sent from a client.
"""
def extract(request, session_config = %__MODULE__{}) do
case Raxx.get_header(request, "cookie") do
nil ->
{:error, :no_cookies_sent}
cookie_header ->
case Cookie.parse(cookie_header) do
cookies = %{} ->
case Map.fetch(cookies, "#{session_config.cookie_name}") do
{:ok, session_cookie} ->
case String.split(session_cookie, "--", parts: 2) do
[payload, digest] ->
if verify_signature(payload, digest, [
session_config.secret | session_config.previous_secrets
]) do
safe_decode(payload)
else
{:error, :could_not_verify_signature}
end
_ ->
{:error, :invalid_session_cookie}
end
:error ->
{:error, :no_session_cookie}
end
end
end
end
@doc """
Sent a response to client informing it to clear a session.
"""
def expire(response, session_config = %__MODULE__{}) do
response
|> Raxx.set_header("set-cookie", SetCookie.expire(session_config.cookie_name))
end
defp safe_digest(payload, secret) do
:crypto.hmac(:sha256, secret, payload)
|> Base.url_encode64()
end
defp safe_encode(term) do
{:ok, encoded_session} = encode(term)
Base.url_encode64(encoded_session)
end
defp encode(term) do
{:ok, :erlang.term_to_binary(term)}
end
defp safe_decode(binary) do
{:ok, encoded} = Base.url_decode64(binary)
decode(encoded)
end
# NOTE make sure decode is only called after verifying digest
# https://elixirforum.com/t/static-and-session-security-fixes-for-plug/3913
defp decode(binary) do
try do
term = :erlang.binary_to_term(binary)
{:ok, term}
rescue
_e in ArgumentError ->
{:error, :unable_to_decode_session}
end
end
defp verify_signature(payload, digest, secrets) do
Enum.any?(secrets, fn secret ->
secure_compare(digest, safe_digest(payload, secret))
end)
end
defp secure_compare(left, right) do
if byte_size(left) == byte_size(right) do
secure_compare(left, right, 0) == 0
else
false
end
end
defp secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
import Bitwise
xorred = x ^^^ y
secure_compare(left, right, acc ||| xorred)
end
defp secure_compare(<<>>, <<>>, acc) do
acc
end
end