Generic entitlement validation using RSA signatures and canonical JSON.
Entitlements are JSON documents signed with RSA-SHA256. The entire payload (all fields except 'signature') is canonically serialized before signing. This design is reusable for any operation type that requires signed metadata.
The validation ensures:
- The token is valid JSON
- The RSA signature matches the payload
- The entitlement_id in the token matches the expected ID
- No field was tampered with after signing
Signature Format
Signatures are computed as:
signature = Base64.encode(RSA_SIGN(SHA256(canonical_json), private_key))where canonical_json is the JSON representation with:
- All fields except "signature"
- Keys sorted alphabetically
- Whitespace normalized
This allows any application holding the RSA public key to verify the token without needing to know how it was generated.
Note: This module uses Erlang :public_key built-in module which is not known at compile time but available at runtime. Compile warnings are suppressed with @compile {:nowarn_undefined}.
Summary
Functions
Extracts entitlement token from HTTP header.
Signs canonical JSON with RSA private key.
Signs an entitlement claims map and returns a JSON string with signature.
Validates an entitlement token (JSON with RSA signature).
Functions
@spec extract_from_header(Plug.Conn.t(), binary()) :: {:ok, binary()} | {:error, :missing | :multiple}
Extracts entitlement token from HTTP header.
By default, looks for the X-Entitlement header, but a custom header name can be specified.
Parameters
conn: Plug.Conn connectionheader_name: Name of the header to extract (default: "x-entitlement")
Returns {:ok, token} if exactly one header present, {:error, :missing} if absent,
or {:error, :multiple} if multiple headers present.
Example
iex> extract_from_header(conn)
{:ok, "{\"entitlement_id\":\"...\", ...}"}
iex> extract_from_header(conn, "x-custom-auth")
{:ok, token}
Signs canonical JSON with RSA private key.
Parameters
canonical_json: JSON string (should be canonically formatted)private_key_pem: RSA private key in PEM format
Returns
{:ok, signature}: Base64-encoded signature{:error, :invalid_key}: Key could not be decoded
Example
iex> canonical_json = "{\"key\":\"value\"}"
iex> sign_canonical_json(canonical_json, private_key_pem)
{:ok, "base64-encoded-signature"}
Signs an entitlement claims map and returns a JSON string with signature.
The signature is computed over the canonical JSON representation (all fields sorted by key), then encoded as Base64 and added to the token.
Parameters
claims: Map with entitlement data (keys should be strings)private_key_pem: RSA private key in PEM format
Returns
{:ok, token_json}: JSON string with signature field added{:error, :invalid_key}: Private key could not be decoded{:error, :signing_failed}: Signature operation failed
Example
iex> claims = %{
"entitlement_id" => "967376a7-6a33-4bc8-846c-ca443c5c56eb",
"user_id" => "user-123",
"operation" => "file_upload"
}
iex> sign_token(claims, private_key_pem)
{:ok, "{\"entitlement_id\":\"...\"...}"}
@spec validate(binary(), binary(), binary()) :: {:ok, map()} | {:error, :invalid_json | :invalid_signature | :invalid_claims}
Validates an entitlement token (JSON with RSA signature).
The token is a JSON string containing all authorization data and a Base64-encoded RSA signature. The entire JSON (minus the signature field) must be valid under the public key.
Parameters
token_json: JSON string from X-Entitlement header (must be valid JSON)public_key_pem: RSA public key in PEM formatentitlement_id: UUID to match against token's entitlement_id field
Returns
{:ok, claims}: All fields from the token JSON (including signature field){:error, :invalid_json}: Token is not valid JSON{:error, :invalid_signature}: RSA signature verification failed{:error, :invalid_claims}: Signature field missing, invalid format, or entitlement_id mismatch
Example
iex> validate(token_json, pem_key, "967376a7-6a33-4bc8-846c-ca443c5c56eb")
{:ok, %{"entitlement_id" => "...", "user_id" => "...", "operation" => "...", ...}}