Session fingerprinting utilities for preventing session hijacking.
This module provides functions to create and verify session fingerprints based on IP address and user agent data. These fingerprints help detect when a session token is being used from a different location or device than where it was created.
Security Considerations
- IP addresses can change (mobile users, VPNs, etc.), so strict enforcement may impact legitimate users
- User agents can be spoofed, but provide an additional layer of verification
- This is defense-in-depth: fingerprinting complements, not replaces, other security measures
Configuration
You can configure the strictness level in your application config:
config :phoenix_kit,
session_fingerprint_enabled: true,
session_fingerprint_strict: false # true = force re-auth, false = log warningsExamples
# Create a fingerprint from a connection
fingerprint = SessionFingerprint.create_fingerprint(conn)
# Verify a fingerprint
case SessionFingerprint.verify_fingerprint(conn, stored_ip, stored_ua_hash) do
:ok -> # Fingerprint matches
{:warning, :ip_mismatch} -> # IP changed, but might be legitimate
{:warning, :user_agent_mismatch} -> # User agent changed
{:error, :fingerprint_mismatch} -> # Both changed, likely hijacked
end
Summary
Functions
Creates a session fingerprint from a Plug.Conn connection.
Checks if session fingerprinting is enabled in the application config.
Extracts the IP address from a connection.
Extracts and hashes the user agent from a connection.
The visitor's address to move a session to when the one it was stored with was a reverse proxy's — or nil when the stored address stands.
Checks if strict fingerprint verification is enabled.
Verifies a session fingerprint against the current connection.
Types
Functions
Creates a session fingerprint from a Plug.Conn connection.
Returns a %SessionFingerprint{} struct with :ip_address and :user_agent_hash fields.
Examples
iex> create_fingerprint(conn)
%SessionFingerprint{ip_address: "192.168.1.1", user_agent_hash: "a1b2c3d4..."}
Checks if session fingerprinting is enabled in the application config.
Examples
iex> fingerprinting_enabled?()
true
Extracts the IP address from a connection.
Delegates to PhoenixKit.Utils.IpAddress.extract_from_conn/1: a forwarded
header is trusted only when conn.remote_ip is itself a loopback or
private address (a proxy on the same box or network), and only the LAST
x-forwarded-for entry — the one a proxy appends, not the one a client
sent — is used. The previous implementation trusted x-forwarded-for
unconditionally and took the FIRST entry, so anyone could spoof this
fingerprint's IP (and therefore the "new device" / session-hijack checks
built on it) with a plain client-supplied header, proxy or no proxy.
Examples
iex> get_ip_address(conn)
"192.168.1.1"
Extracts and hashes the user agent from a connection.
Returns a SHA256 hash of the user agent string for privacy and storage efficiency.
Examples
iex> hash_user_agent(conn)
"a1b2c3d4e5f6..."
@spec proxy_rebind_address(Plug.Conn.t(), String.t() | nil, String.t() | nil) :: String.t() | nil
The visitor's address to move a session to when the one it was stored with was a reverse proxy's — or nil when the stored address stands.
Before core could read the visitor's address through a proxy that writes
ip:port (Caddy's {remote}), a session signed in behind one stored the
proxy's own loopback or private address. Read correctly now, every
request of such a session would be a changed IP: a warning each time, a
sign-out under strict mode. A request that came through a proxy (its
peer is loopback/private), from the same browser, for a session stored
with a loopback/private address and now read as a public one, is that
session — it moves to the current address once and is checked as usual
from then on.
This is a deliberate, one-off loosening, not a defence: a user agent is easy to copy, so a stolen token with the browser's user agent, sent through the proxy, is moved too. What bounds it: a session stored with a private/loopback address, used again through a private peer (a proxy) from a public address, moves once — after it, the stored address is public and never moves again. That includes a LAN client that signed in through the same proxy (its forwarded address private too). A request straight from a public peer and a different browser are not moved.
Checks if strict fingerprint verification is enabled.
When strict mode is enabled, fingerprint mismatches will force re-authentication. When disabled, mismatches only log warnings.
Examples
iex> strict_mode?()
false
Verifies a session fingerprint against the current connection.
Returns:
:okif fingerprint matches{:warning, :ip_mismatch}if only IP changed{:warning, :user_agent_mismatch}if only user agent changed{:error, :fingerprint_mismatch}if both changed:okif stored fingerprint is nil (backward compatibility)
Examples
iex> verify_fingerprint(conn, "192.168.1.1", "abc123")
:ok
iex> verify_fingerprint(conn, "10.0.0.1", "abc123")
{:warning, :ip_mismatch}