dev_codec_cookie.erl - Cookie Management and Authentication Device
Overview
Purpose: HTTP cookie management with HMAC-based authentication
Module: dev_codec_cookie
Device Name: cookie@1.0
Codec Format: cookie@1.0
This utility device manages HTTP cookies for requests, providing encoding/decoding, storage, and HMAC-based authentication using the ~message@1.0 codec interface. It implements the generator interface, enabling integration with ~auth-hook@1.0 for automatic request signing based on cookie-stored secrets.
Core Capabilities
- Cookie Parsing: Parse
cookieandset-cookieheaders - Cookie Encoding: Generate properly formatted cookie strings
- Storage Management: Store/retrieve cookies in message private data
- HMAC Authentication: Sign requests using cookie-stored secrets
- Generator Interface: Automatic secret generation and management
- Format Conversion: Between
cookie,set-cookie, and structured formats
Dependencies
- HyperBEAM:
hb_private,hb_maps,hb_ao,hb_util,hb_escape,hb_cache - Authentication:
dev_codec_cookie_auth(separate module) - Includes:
include/hb.hrl - Testing:
eunit
Public Functions Overview
%% Cookie Operations
-spec get_cookie(Base, Req, Opts) -> {ok, Cookie} | {error, not_found}.
-spec store(Base, Req, Opts) -> {ok, UpdatedBase}.
-spec extract(Base, Req, Opts) -> {ok, Cookies}.
-spec reset(Base, Opts) -> {ok, CleanBase}.
%% Format Conversion (Codec API)
-spec to(Msg, Req, Opts) -> {ok, FormattedMsg}.
-spec from(Msg, Req, Opts) -> {ok, ParsedMsg}.
%% Authentication (Commitment API)
-spec commit(Base, Req, Opts) -> {ok, SignedMsg}.
-spec verify(Base, Req, Opts) -> {ok, boolean()}.
%% Generator Interface
-spec generate(Base, Req, Opts) -> {ok, NormalizedReq}.
-spec finalize(Base, Request, Opts) -> {ok, FinalSequence}.
%% Utilities
-spec opts(Opts) -> PrivateOpts.Public Functions
1. get_cookie/3
-spec get_cookie(Base, Req, Opts) -> {ok, Cookie} | {error, not_found}
when
Base :: map(),
Req :: map(),
Opts :: map(),
Cookie :: binary() | map().Description: Get a specific cookie by key from the base message. Format controlled by format option in request.
<<"default">>- Raw form (binary or map)<<"set-cookie">>- Normalized map with value, attributes, flags<<"cookie">>- Binary value only
-module(dev_codec_cookie_get_test).
-include_lib("eunit/include/eunit.hrl").
get_cookie_default_test() ->
Base = hb_private:set(#{}, <<"cookie">>, #{
<<"session">> => <<"abc123">>
}, hb_private:opts(#{})),
Req = #{ <<"key">> => <<"session">> },
{ok, Cookie} = dev_codec_cookie:get_cookie(Base, Req, #{}),
?assertEqual(<<"abc123">>, Cookie).
get_cookie_set_format_test() ->
Base = hb_private:set(#{}, <<"cookie">>, #{
<<"session">> => <<"value">>
}, hb_private:opts(#{})),
Req = #{
<<"key">> => <<"session">>,
<<"format">> => <<"set-cookie">>
},
{ok, Cookie} = dev_codec_cookie:get_cookie(Base, Req, #{}),
?assert(is_map(Cookie)),
?assertEqual(<<"value">>, maps:get(<<"value">>, Cookie)).
get_cookie_not_found_test() ->
Base = #{},
Req = #{ <<"key">> => <<"missing">> },
?assertEqual({error, not_found},
dev_codec_cookie:get_cookie(Base, Req, #{})).2. store/3
-spec store(Base, Req, Opts) -> {ok, UpdatedBase}
when
Base :: map(),
Req :: map(),
Opts :: map(),
UpdatedBase :: map().Description: Store keys from request into cookies, merging with existing cookies. Filters out base keys like path, method, body, etc.
-module(dev_codec_cookie_store_test).
-include_lib("eunit/include/eunit.hrl").
store_simple_test() ->
Base = #{},
Req = #{
<<"session">> => <<"abc123">>,
<<"user">> => <<"john">>
},
{ok, Updated} = dev_codec_cookie:store(Base, Req, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Updated, #{}, #{}),
?assertEqual(<<"abc123">>, maps:get(<<"session">>, Cookies)),
?assertEqual(<<"john">>, maps:get(<<"user">>, Cookies)).
store_merge_test() ->
Opts = hb_private:opts(#{}),
Base = hb_private:set(#{}, <<"cookie">>, #{
<<"existing">> => <<"value">>
}, Opts),
Req = #{ <<"new">> => <<"data">> },
{ok, Updated} = dev_codec_cookie:store(Base, Req, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Updated, #{}, #{}),
?assertEqual(<<"value">>, maps:get(<<"existing">>, Cookies)),
?assertEqual(<<"data">>, maps:get(<<"new">>, Cookies)).
store_filters_base_keys_test() ->
Base = #{},
Req = #{
<<"data">> => <<"keep">>,
<<"path">> => <<"/ignored">>,
<<"method">> => <<"POST">>,
<<"body">> => <<"ignored">>
},
{ok, Updated} = dev_codec_cookie:store(Base, Req, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Updated, #{}, #{}),
?assert(maps:is_key(<<"data">>, Cookies)),
?assertNot(maps:is_key(<<"path">>, Cookies)),
?assertNot(maps:is_key(<<"method">>, Cookies)).3. extract/3
-spec extract(Base, Req, Opts) -> {ok, Cookies}
when
Base :: map(),
Req :: map(),
Opts :: map(),
Cookies :: map().Description: Extract and parse all cookies from a message, returning normalized cookie map.
Test Code:-module(dev_codec_cookie_extract_test).
-include_lib("eunit/include/eunit.hrl").
extract_from_cookie_header_test() ->
Msg = #{ <<"cookie">> => <<"key1=value1; key2=value2">> },
{ok, Cookies} = dev_codec_cookie:extract(Msg, #{}, #{}),
?assertEqual(<<"value1">>, maps:get(<<"key1">>, Cookies)),
?assertEqual(<<"value2">>, maps:get(<<"key2">>, Cookies)).
extract_from_set_cookie_test() ->
Msg = #{ <<"set-cookie">> => [<<"session=abc123; Path=/">>] },
{ok, Cookies} = dev_codec_cookie:extract(Msg, #{}, #{}),
SessionCookie = maps:get(<<"session">>, Cookies),
?assertEqual(<<"abc123">>, maps:get(<<"value">>, SessionCookie)).
extract_empty_test() ->
Msg = #{},
{ok, Cookies} = dev_codec_cookie:extract(Msg, #{}, #{}),
?assertEqual(#{}, Cookies).4. reset/2
-spec reset(Base, Opts) -> {ok, CleanBase}
when
Base :: map(),
Opts :: map(),
CleanBase :: map().Description: Remove all cookie-related keys from a message, including cookie, set-cookie, and priv/cookie.
-module(dev_codec_cookie_reset_test).
-include_lib("eunit/include/eunit.hrl").
reset_test() ->
Opts = hb_private:opts(#{}),
Base = #{
<<"cookie">> => <<"key=val">>,
<<"set-cookie">> => [<<"session=abc">>],
<<"other">> => <<"keep">>
},
BaseWithPriv = hb_private:set(Base, <<"cookie">>, #{}, Opts),
{ok, Clean} = dev_codec_cookie:reset(BaseWithPriv, #{}),
?assertNot(maps:is_key(<<"cookie">>, Clean)),
?assertNot(maps:is_key(<<"set-cookie">>, Clean)),
?assertEqual(<<"keep">>, maps:get(<<"other">>, Clean)).5. to/3
-spec to(Msg, Req, Opts) -> {ok, FormattedMsg}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
FormattedMsg :: map().Description: Convert message with cookie sources to specified format (set-cookie or cookie header format).
<<"set-cookie">>- List of Set-Cookie header lines<<"cookie">>- Single Cookie header line
-module(dev_codec_cookie_to_test).
-include_lib("eunit/include/eunit.hrl").
to_set_cookie_format_test() ->
Opts = hb_private:opts(#{}),
Msg = hb_private:set(#{}, <<"cookie">>, #{
<<"session">> => #{
<<"value">> => <<"abc123">>,
<<"attributes">> => #{ <<"Path">> => <<"/">> },
<<"flags">> => [<<"HttpOnly">>]
}
}, Opts),
Req = #{ <<"format">> => <<"set-cookie">> },
{ok, Result} = dev_codec_cookie:to(Msg, Req, #{}),
SetCookies = maps:get(<<"set-cookie">>, Result),
?assert(is_list(SetCookies)),
[Line] = SetCookies,
% Value is quoted: session="abc123"
?assert(binary:match(Line, <<"session=">>) =/= nomatch),
?assert(binary:match(Line, <<"abc123">>) =/= nomatch).
to_cookie_format_test() ->
Opts = hb_private:opts(#{}),
Msg = hb_private:set(#{}, <<"cookie">>, #{
<<"key1">> => <<"val1">>,
<<"key2">> => <<"val2">>
}, Opts),
Req = #{ <<"format">> => <<"cookie">> },
{ok, Result} = dev_codec_cookie:to(Msg, Req, #{}),
Cookie = maps:get(<<"cookie">>, Result),
% Values are quoted: key1="val1"
?assert(binary:match(Cookie, <<"key1=">>) =/= nomatch),
?assert(binary:match(Cookie, <<"val1">>) =/= nomatch).6. from/3
-spec from(Msg, Req, Opts) -> {ok, ParsedMsg}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
ParsedMsg :: map().Description: Parse cookies from cookie or set-cookie headers and store in private data area.
-module(dev_codec_cookie_from_test).
-include_lib("eunit/include/eunit.hrl").
from_cookie_header_test() ->
Msg = #{ <<"cookie">> => <<"session=abc123; user=john">> },
{ok, Parsed} = dev_codec_cookie:from(Msg, #{}, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Parsed, #{}, #{}),
?assertEqual(<<"abc123">>, maps:get(<<"session">>, Cookies)),
?assertEqual(<<"john">>, maps:get(<<"user">>, Cookies)).
from_set_cookie_header_test() ->
Msg = #{
<<"set-cookie">> => [
<<"session=abc; Path=/; HttpOnly">>,
<<"user=john; Secure">>
]
},
{ok, Parsed} = dev_codec_cookie:from(Msg, #{}, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Parsed, #{}, #{}),
Session = maps:get(<<"session">>, Cookies),
?assertEqual(<<"abc">>, maps:get(<<"value">>, Session)),
?assert(lists:member(<<"HttpOnly">>, maps:get(<<"flags">>, Session))).7. commit/3, verify/3
-spec commit(Base, Req, Opts) -> {ok, SignedMsg}.
-spec verify(Base, Req, Opts) -> {ok, boolean()}.Description: Sign message using HMAC with cookie-stored secret, or verify such signatures. Delegates to dev_codec_cookie_auth module.
-module(dev_codec_cookie_commit_test).
-include_lib("eunit/include/eunit.hrl").
commit_test() ->
% Test that commit function generates a secret and signs
Opts = hb_private:opts(#{}),
Base = hb_private:set(#{}, <<"cookie">>, #{}, Opts),
Req = #{ <<"data">> => <<"test">> },
{ok, Signed} = dev_codec_cookie:commit(Base, Req, #{}),
% Should have commitments
?assert(maps:is_key(<<"commitments">>, Signed)).8. generate/3
-spec generate(Base, Req, Opts) -> {ok, NormalizedReq}
when
Base :: map(),
Req :: map(),
Opts :: map(),
NormalizedReq :: term().Description: Generate or retrieve secret from cookies for authentication. Part of the generator interface for use with ~auth-hook@1.0.
-module(dev_codec_cookie_generate_test).
-include_lib("eunit/include/eunit.hrl").
generate_new_secret_test() ->
Base = #{},
Req = #{},
Result = dev_codec_cookie:generate(Base, Req, #{}),
% Should generate new secret
?assertMatch({ok, _}, Result).9. finalize/3
-spec finalize(Base, Request, Opts) -> {ok, FinalSequence}
when
Base :: map(),
Request :: map(),
Opts :: map(),
FinalSequence :: list().Description: Add set-cookie header to message sequence after hook processing. Part of the generator interface.
-module(dev_codec_cookie_finalize_test).
-include_lib("eunit/include/eunit.hrl").
finalize_test() ->
Base = #{},
Request = #{},
Result = dev_codec_cookie:finalize(Base, Request, #{}),
% Returns error when no request context available
?assertMatch({error, _}, Result).Cookie Formats
Cookie Header
Simple key=value pairs:
Cookie: session=abc123; user=john; theme=darkParsed to:
#{
<<"session">> => <<"abc123">>,
<<"user">> => <<"john">>,
<<"theme">> => <<"dark">>
}Set-Cookie Header
With attributes and flags:
Set-Cookie: session=abc123; Path=/; Domain=example.com; HttpOnly; SecureParsed to:
#{
<<"session">> => #{
<<"value">> => <<"abc123">>,
<<"attributes">> => #{
<<"Path">> => <<"/">>,
<<"Domain">> => <<"example.com">>
},
<<"flags">> => [<<"HttpOnly">>, <<"Secure">>]
}
}Generator Interface
Purpose
Enables integration with ~auth-hook@1.0 for automatic request signing based on cookie-stored secrets.
Implementation
generate/3:- Retrieves or creates secret in cookies
- Returns request with secret available
- Adds set-cookie to response sequence
- Ensures client receives cookie updates
Usage with Auth Hook
#{
on => #{
<<"request">> => #{
<<"device">> => <<"auth-hook@1.0">>,
<<"secret-provider">> => #{
<<"device">> => <<"cookie@1.0">>
}
}
}
}HMAC Authentication
Commitment Type
Uses httpsig@1.0 HMAC scheme:
#{
<<"type">> => <<"hmac-sha256">>,
<<"secret">> => SecretHash,
<<"committer">> => SecretHash
}Secret Storage
Secret stored in cookies with key = hash(secret):
SecretHash = crypto:hash(sha256, Secret),
CookieKey = hb_util:encode(SecretHash)Common Patterns
%% Parse incoming cookies
Msg = #{ <<"cookie">> => <<"session=abc; user=john">> },
{ok, Parsed} = dev_codec_cookie:from(Msg, #{}, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Parsed, #{}, #{}).
%% Store cookies in message
Base = #{},
Req = #{ <<"session">> => <<"abc123">>, <<"user">> => <<"john">> },
{ok, Updated} = dev_codec_cookie:store(Base, Req, #{}).
%% Convert to Set-Cookie headers
{ok, WithSetCookie} = dev_codec_cookie:to(
Updated,
#{ <<"format">> => <<"set-cookie">> },
#{}
),
SetCookies = maps:get(<<"set-cookie">>, WithSetCookie).
%% Get specific cookie
{ok, SessionCookie} = dev_codec_cookie:get_cookie(
Base,
#{ <<"key">> => <<"session">> },
#{}
).
%% HMAC authentication with cookies
Secret = crypto:strong_rand_bytes(32),
Opts = hb_private:opts(#{}),
BaseWithSecret = hb_private:set(#{}, <<"cookie">>, #{
<<"secret">> => Secret
}, Opts),
{ok, Signed} = dev_codec_cookie:commit(BaseWithSecret, Msg, #{}),
{ok, true} = dev_codec_cookie:verify(BaseWithSecret, Signed, #{}).
%% Clean cookies from message
{ok, Clean} = dev_codec_cookie:reset(MsgWithCookies, #{}).
%% Use with auth hook
NodeConfig = #{
on => #{
<<"request">> => #{
<<"device">> => <<"auth-hook@1.0">>,
<<"secret-provider">> => #{
<<"device">> => <<"cookie@1.0">>
}
}
}
},
Node = hb_http_server:start_node(NodeConfig).Cookie Attributes
Common Attributes
- Path: URL path scope
- Domain: Domain scope
- Expires: Expiration date
- Max-Age: Lifetime in seconds
- SameSite: CSRF protection (Strict/Lax/None)
Common Flags
- Secure: HTTPS only
- HttpOnly: No JavaScript access
- SameSite: Default cross-site policy
URI Encoding
Values automatically URI-encoded/decoded:
% Input: "hello world"
% Encoded: "hello%20world"
% Decoded: "hello world"Private Data Storage
Cookies stored in message private data:
hb_private:set(Msg, <<"cookie">>, CookieMap, Opts)
hb_private:get(<<"cookie">>, Msg, Default, Opts)Filtered Keys
These keys excluded when storing from request:
<<"path">><<"accept-bundle">><<"ao-peer">><<"host">><<"method">><<"body">>
References
- Authentication -
dev_codec_cookie_auth.erl - Private Data -
hb_private.erl - Auth Hook -
dev_auth_hook.erl - HTTP Signature -
dev_codec_httpsig.erl
Notes
- Codec Interface: Implements
~message@1.0format conversion - Generator Interface: For integration with auth hooks
- HMAC Authentication: Cookie-based secret storage
- Format Support: Cookie and Set-Cookie headers
- Attribute Preservation: Full support for cookie attributes
- Flag Support: HttpOnly, Secure, SameSite, etc.
- URI Encoding: Automatic encoding/decoding
- Private Storage: Uses hb_private for cookie data
- Merge Support: Combines existing and new cookies
- Key Filtering: Excludes request metadata from storage
- Format Conversion: Between header formats
- Parse Robustness: Handles various cookie formats
- Secret Generation: Automatic for authentication
- Finalization: Adds cookies to response sequence
- Clean Operations: Complete cookie removal support