Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol core session security_session.ex
Raw

lib/raxol/core/session/security_session.ex

defmodule Raxol.Core.Session.SecuritySession do
alias Raxol.Core.Runtime.Log
@moduledoc """
Security session implementation for the unified session manager.
Provides cryptographically secure session management with:
- Secure token generation and validation
- Session expiration and renewal
- Concurrent session limiting
- Session fixation protection
- CSRF token generation
"""
defstruct [
:id,
:user_id,
:token,
:created_at,
:last_activity,
:expires_at,
:ip_address,
:user_agent,
:metadata
]
@type t :: %__MODULE__{
id: String.t(),
user_id: String.t(),
token: binary(),
created_at: DateTime.t(),
last_activity: DateTime.t(),
expires_at: DateTime.t(),
ip_address: String.t() | nil,
user_agent: String.t() | nil,
metadata: map()
}
## Public API
@doc """
Creates a new security session.
"""
def create(user_id, opts, config, sessions_state) do
case check_session_limit(user_id, config.max_concurrent, sessions_state) do
:ok ->
session = create_new_session(user_id, opts, config)
updated_state = store_session(session, sessions_state)
session_info = %{
session_id: session.id,
token: session.token,
expires_at: session.expires_at
}
{:ok, session_info, updated_state}
{:error, :limit_exceeded} ->
{:error, :session_limit_exceeded}
end
end
@doc """
Validates a session token and returns session info.
"""
def validate(session_id, token, sessions_state) do
case lookup_session(session_id, sessions_state) do
nil ->
{:error, :invalid_session}
session ->
validate_token_and_expiry(session, token, session_id, sessions_state)
end
end
@doc """
Invalidates a session.
"""
def invalidate(session_id, sessions_state) do
invalidate_session_internal(session_id, sessions_state)
end
@doc """
Invalidates all sessions for a user.
"""
def invalidate_user_sessions(user_id, sessions_state) do
sessions = get_user_sessions_internal(user_id, sessions_state)
updated_state =
Enum.reduce(sessions, sessions_state, fn session, acc ->
invalidate_session_internal(session.id, acc)
end)
{length(sessions), updated_state}
end
@doc """
Generates a CSRF token for a session.
"""
def generate_csrf_token(session_id) do
secret = Application.get_env(:raxol, :secret_key_base, "default_secret")
:crypto.mac(:hmac, :sha256, secret, session_id)
|> Base.url_encode64(padding: false)
end
@doc """
Validates a CSRF token.
"""
def validate_csrf_token(session_id, token) do
expected = generate_csrf_token(session_id)
secure_token_compare(expected, token)
end
@doc """
Gets statistics about security sessions.
"""
def get_stats(_sessions_state) do
case Raxol.Core.CompilerState.safe_lookup(:unified_security_sessions, :all) do
{:ok, sessions} ->
%{
total: length(sessions),
active: count_active_sessions(sessions)
}
{:error, :table_not_found} ->
%{total: 0, active: 0}
end
end
@doc """
Counts total security sessions.
"""
def count(_sessions_state) do
case Raxol.Core.CompilerState.safe_lookup(:unified_security_sessions, :all) do
{:ok, sessions} -> length(sessions)
{:error, :table_not_found} -> 0
end
end
@doc """
Cleans up expired security sessions.
"""
def cleanup_expired(sessions_state, _config) do
cleanup_expired_sessions()
sessions_state
end
@doc """
Gets all sessions for a user.
"""
def get_user_sessions(user_id, sessions_state) do
get_user_sessions_internal(user_id, sessions_state)
|> Enum.map(&sanitize_session_info/1)
end
## Private Functions
@spec create_new_session(String.t() | integer(), keyword(), map()) :: any()
defp create_new_session(user_id, opts, config) do
now = DateTime.utc_now()
%__MODULE__{
id: generate_session_id(),
user_id: user_id,
token: generate_secure_token(config.token_bytes),
created_at: now,
last_activity: now,
expires_at: DateTime.add(now, config.timeout, :millisecond),
ip_address: Keyword.get(opts, :ip_address),
user_agent: Keyword.get(opts, :user_agent),
metadata: Keyword.get(opts, :metadata, %{})
}
end
defp generate_session_id do
Base.url_encode64(:crypto.strong_rand_bytes(16), padding: false)
end
@spec generate_secure_token(any()) :: any()
defp generate_secure_token(token_bytes) do
Base.url_encode64(:crypto.strong_rand_bytes(token_bytes), padding: false)
end
@spec store_session(any(), map()) :: any()
defp store_session(session, sessions_state) do
:ets.insert(:unified_security_sessions, {session.id, session})
:ets.insert(:unified_user_security_sessions, {session.user_id, session.id})
sessions_state
end
@spec lookup_session(String.t() | integer(), map()) :: any()
defp lookup_session(session_id, _sessions_state) do
case Raxol.Core.CompilerState.safe_lookup(
:unified_security_sessions,
session_id
) do
{:ok, [{_, session}]} -> session
{:ok, []} -> nil
{:error, :table_not_found} -> nil
end
end
@spec validate_token_and_expiry(any(), any(), String.t() | integer(), map()) ::
{:ok, any()} | {:error, any()}
defp validate_token_and_expiry(session, token, session_id, sessions_state) do
with true <- secure_token_compare(session.token, token),
false <- expired?(session) do
# Update last activity
updated_session = %{session | last_activity: DateTime.utc_now()}
:ets.insert(:unified_security_sessions, {session_id, updated_session})
session_info = %{
user_id: session.user_id,
metadata: session.metadata,
expires_at: session.expires_at
}
{:ok, session_info, sessions_state}
else
false ->
Log.warning("Invalid token for session #{session_id}")
{:error, :invalid_token}
true ->
invalidate_session_internal(session_id, sessions_state)
{:error, :session_expired}
end
end
@spec invalidate_session_internal(String.t() | integer(), map()) :: any()
defp invalidate_session_internal(session_id, sessions_state) do
case lookup_session(session_id, sessions_state) do
nil ->
sessions_state
session ->
:ets.delete(:unified_security_sessions, session_id)
:ets.delete_object(
:unified_user_security_sessions,
{session.user_id, session_id}
)
Log.info("Security session invalidated: #{session_id}")
sessions_state
end
end
@spec get_user_sessions_internal(String.t() | integer(), map()) :: any() | nil
defp get_user_sessions_internal(user_id, _sessions_state) do
case Raxol.Core.CompilerState.safe_lookup(
:unified_user_security_sessions,
user_id
) do
{:ok, sessions} -> sessions
{:error, :table_not_found} -> []
end
|> Enum.map(fn {_, session_id} -> lookup_session(session_id, nil) end)
|> Enum.filter(& &1)
|> Enum.reject(&expired?/1)
end
@spec check_session_limit(String.t() | integer(), any(), map()) :: any()
defp check_session_limit(user_id, max_concurrent, sessions_state) do
active_count = length(get_user_sessions_internal(user_id, sessions_state))
case active_count >= max_concurrent do
true -> {:error, :limit_exceeded}
false -> :ok
end
end
@spec expired?(any()) :: boolean()
defp expired?(session) do
DateTime.compare(DateTime.utc_now(), session.expires_at) == :gt
end
@spec secure_token_compare(any(), any()) :: any()
defp secure_token_compare(token1, token2) do
case byte_size(token1) == byte_size(token2) do
true -> :crypto.hash_equals(token1, token2)
false -> false
end
end
@spec sanitize_session_info(any()) :: any()
defp sanitize_session_info(session) do
%{
id: session.id,
created_at: session.created_at,
last_activity: session.last_activity,
expires_at: session.expires_at,
ip_address: session.ip_address,
user_agent: session.user_agent
}
end
defp cleanup_expired_sessions do
case Raxol.Core.CompilerState.safe_lookup(:unified_security_sessions, :all) do
{:ok, all_sessions} ->
expired =
Enum.filter(all_sessions, fn {_, session} -> expired?(session) end)
Enum.each(expired, fn {session_id, _} ->
invalidate_session_internal(session_id, %{})
end)
if length(expired) > 0 do
Log.info("Cleaned up #{length(expired)} expired security sessions")
end
{:error, :table_not_found} ->
:ok
end
end
@spec count_active_sessions(any()) :: any()
defp count_active_sessions(sessions) do
Enum.count(sessions, fn {_, session} -> not expired?(session) end)
end
end