View Source Charon.SessionPlugs (Charon v3.0.3)
Plugs to create, update/refresh and delete sessions. When creating or updating a session, new tokens are created as well.
Link to this section Summary
Functions
Delete the persistent session identified by the session_id in the token claims.
Create or update a session. If a session exists in the conn, the session is updated / refreshed, otherwise a new one is created.
Refresh an existing session by putting it on the connection with Charon.TokenPlugs.load_session/2
.
Link to this section Types
Link to this section Functions
@spec delete_session(Plug.Conn.t(), Charon.Config.t()) :: Plug.Conn.t()
Delete the persistent session identified by the session_id in the token claims.
Note that the token remains valid until it expires, it is left up to the client to drop the access token. It will no longer be possible to refresh the session, however.
examples-doctests
Examples / doctests
# instructs browsers to clear signature cookies
iex> conn()
...> |> Plug.Test.put_req_cookie(@config.access_cookie_name, "anything")
...> |> Plug.Test.put_req_cookie(@config.refresh_cookie_name, "anything")
...> |> delete_session(@config)
...> |> Conn.fetch_cookies()
...> |> Map.get(:cookies)
%{}
@spec upsert_session(Plug.Conn.t(), Charon.Config.t(), upsert_session_opts()) :: Plug.Conn.t()
Create or update a session. If a session exists in the conn, the session is updated / refreshed, otherwise a new one is created.
Refresh an existing session by putting it on the connection with Charon.TokenPlugs.load_session/2
.
In both cases, new access / refresh tokens are created and stored in the conn's private map. The server-side session stored in the session store is created / updated as well.
If a new session is created, this plug must be preceded by Charon.Utils.set_token_signature_transport/2
and Charon.Utils.set_user_id/2
or an error will be raised.
The tokens' signatures are split off and sent as cookies if the session's token signature transport mechanism is set to :cookie
. By default, these are http-only strictly-same-site secure cookies.
Optionally, it is possible to add extra claims to the access- and refresh tokens or to store extra payload in the server-side session.
Session stores may return an optimistic locking error, meaning there are concurrent updates to a session.
In this case, upsert/3
will raise a Charon.SessionPlugs.SessionUpdateConflictError
, which should result
in an HTTP 409 Conflict error.
If the session store returns another error, a Charon.SessionPlugs.SessionStorageError
is raised,
which is an unrecoverable state that should result in an HTTP 500 Internal Server Error.
claims
Claims
The following claims are set by default:
"exp"
expires at (this value is guaranteed to never outlive the session itself)"iat"
time of token creation"iss"
issuer, usually a url like "https://myapp.com""jti"
jwt id, random unique id for the token (a refresh token's id is stored in the session as well)"nbf"
not before, same value as"iat"
but means "token not valid before this time""sid"
session id"sub"
subject, the user id of the session owner"type"
type,"access"
or"refresh"
"styp"
session type
Additional claims or overrides can be provided with opts
.
examples-doctests
Examples / doctests
# error if user id not set for new session
iex> %Conn{} |> Utils.set_token_signature_transport(:bearer) |> upsert_session(@config)
** (RuntimeError) Set user id using Utils.set_user_id/2
# error if signature transport not set for new session
iex> %Conn{} |> Utils.set_user_id(1) |> upsert_session(@config)
** (RuntimeError) Set token signature transport using Utils.set_token_signature_transport/2
# creates session if none present in conn
iex> conn = conn()
...> |> Utils.set_user_id(1)
...> |> Utils.set_token_signature_transport(:bearer)
...> |> upsert_session(@config)
iex> %Session{} = Utils.get_session(conn)
iex> %Tokens{} = Utils.get_tokens(conn)
# works with infinite lifespan sessions
iex> conn = conn()
...> |> Utils.set_user_id(1)
...> |> Utils.set_token_signature_transport(:bearer)
...> |> upsert_session(%{@config | session_ttl: :infinite})
iex> %Session{expires_at: :infinite} = Utils.get_session(conn)
iex> %Tokens{} = Utils.get_tokens(conn)
# renews session if present in conn, updating only refresh_tokens, refreshed_at, and refresh_expires_at
# existing session's user id will not change despite attempted override
iex> old_session = test_session(user_id: 43, id: "a", expires_at: :infinite, refresh_expires_at: 0, refreshed_at: 0)
iex> conn = conn()
...> |> Conn.put_private(@session, old_session)
...> |> Utils.set_token_signature_transport(:bearer)
...> |> Utils.set_user_id(1)
...> |> upsert_session(@config)
iex> session = Utils.get_session(conn) |> Map.from_struct()
iex> old_session = Map.from_struct(old_session)
iex> Enum.find(~w(id user_id created_at expires_at)a, & session[&1] != old_session[&1])
nil
iex> Enum.find(~w(refresh_token_id refreshed_at refresh_expires_at)a, & session[&1] == old_session[&1])
nil
# returns signatures in cookies if requested, which removes signatures from tokens
iex> conn = conn()
...> |> Utils.set_token_signature_transport(:cookie)
...> |> Utils.set_user_id(1)
...> |> upsert_session(@config)
iex> cookies = conn |> Conn.fetch_cookies() |> Map.get(:cookies)
iex> <<_access_sig::binary>> = Map.get(cookies, @config.access_cookie_name)
iex> <<_refresh_sig::binary>> = Map.get(cookies, @config.refresh_cookie_name)
iex> true = Regex.match?(~r/\w+\.\w+\./, conn |> Utils.get_tokens() |> Map.get(:access_token))
iex> true = Regex.match?(~r/\w+\.\w+\./, conn |> Utils.get_tokens() |> Map.get(:refresh_token))
# tokens get a lot of default claims
iex> conn = conn()
...> |> Utils.set_token_signature_transport(:bearer)
...> |> Utils.set_user_id(1)
...> |> upsert_session(@config)
iex> %{"exp" => _, "iat" => _, "iss" => "my_test_app", "jti" => <<_::binary>>, "nbf" => _, "sid" => <<sid::binary>>, "sub" => 1, "type" => "access", "styp" => "full"} = get_private(conn, @access_token_payload)
iex> %{"exp" => _, "iat" => _, "iss" => "my_test_app", "jti" => <<_::binary>>, "nbf" => _, "sid" => ^sid, "sub" => 1, "type" => "refresh", "styp" => "full"} = get_private(conn, @refresh_token_payload)
# allows adding extra claims to tokens
iex> conn = conn()
...> |> Utils.set_token_signature_transport(:bearer)
...> |> Utils.set_user_id(1)
...> |> upsert_session(@config, access_claim_overrides: %{"much" => :extra}, refresh_claim_overrides: %{"really" => true})
iex> %{"much" => :extra} = get_private(conn, @access_token_payload)
iex> %{"really" => true} = get_private(conn, @refresh_token_payload)
# allows adding extra payload to session
iex> conn = conn()
...> |> Utils.set_user_id(1)
...> |> Utils.set_token_signature_transport(:bearer)
...> |> upsert_session(@config, extra_session_payload: %{what?: "that's right!"})
iex> %Session{extra_payload: %{what?: "that's right!"}} = Utils.get_session(conn)
# allows separating sessions by type (default :full)
iex> conn = conn()
...> |> Utils.set_token_signature_transport(:bearer)
...> |> Utils.set_user_id(1)
...> |> upsert_session(@config, session_type: :oauth2)
iex> %Session{type: :oauth2} = Utils.get_session(conn)
iex> %{"styp" => "oauth2"} = get_private(conn, @access_token_payload)