Skip to content

Authentication

A beginner's guide to identity and signatures in HyperBEAM


What You'll Learn

By the end of this tutorial, you'll understand:

  1. dev_auth_hook — Automatic request signing with node-hosted wallets
  2. dev_codec_http_auth — HTTP Basic authentication with PBKDF2
  3. dev_codec_cookie_auth — Cookie-based HMAC authentication
  4. dev_codec_cookie — Cookie management and storage
  5. dev_secret — Secret key management device

These devices form the authentication layer for identity and signatures.


The Big Picture

HyperBEAM's authentication system provides multiple ways to sign requests:

                    ┌─────────────────────────────────────────────┐
                    │           Authentication Layer              │
                    │                                             │
   Request ───────→ │   ┌─────────────┐    ┌─────────────────┐   │
                    │   │ Auth Hook   │ ─→ │ Secret Provider │   │
                    │   └──────┬──────┘    └────────┬────────┘   │
                    │          │                    │             │
                    │          ▼                    ▼             │
                    │   ┌─────────────┐    ┌─────────────────┐   │
                    │   │  Generate   │ ─→ │   Wallet Gen    │   │
                    │   │   Secret    │    │  (Deterministic)│   │
                    │   └──────┬──────┘    └────────┬────────┘   │
                    │          │                    │             │
                    │          ▼                    ▼             │
   Signed Req ←──── │   ┌─────────────┐    ┌─────────────────┐   │
                    │   │    Sign     │ ─→ │    Finalize     │   │
                    │   │   Request   │    │   (Set Cookie)  │   │
                    │   └─────────────┘    └─────────────────┘   │
                    │                                             │
                    └─────────────────────────────────────────────┘

Think of it like a multi-factor authentication system:

  • dev_auth_hook = Login gateway (coordinates authentication)
  • dev_codec_http_auth = Username/password (HTTP Basic)
  • dev_codec_cookie_auth = Session token (cookie-based)
  • dev_secret = Key vault (manages secrets)

Let's explore each component.


Part 1: Authentication Hook

📖 Reference: dev_auth_hook

dev_auth_hook automatically signs incoming requests using node-hosted wallets. It's designed for trusted environments like TEEs or personal nodes.

How the Auth Hook Works

1. Request arrives


2. Check activation conditions (when)

         ├─ Not relevant → Return unchanged


3. Call secret provider (generate)


4. Generate wallet from secret


5. Sign request and messages


6. Call secret provider (finalize)


7. Return signed request

Configuring the Auth Hook

%% Basic cookie-based authentication
Node = hb_http_server:start_node(#{
    priv_wallet => ar_wallet:new(),
    on => #{
        <<"request">> => #{
            <<"device">> => <<"auth-hook@1.0">>,
            <<"path">> => <<"request">>,
            <<"secret-provider">> => #{
                <<"device">> => <<"cookie@1.0">>
            }
        }
    }
}).

With HTTP Basic Auth

%% HTTP Basic authentication (triggers browser login prompt)
Node = hb_http_server:start_node(#{
    priv_wallet => ar_wallet:new(),
    on => #{
        <<"request">> => #{
            <<"device">> => <<"auth-hook@1.0">>,
            <<"path">> => <<"request">>,
            <<"secret-provider">> => #{
                <<"device">> => <<"http-auth@1.0">>,
                <<"access-control">> => #{
                    <<"device">> => <<"http-auth@1.0">>
                }
            }
        }
    }
}).

Activation Conditions

Control when the hook activates:

#{
    <<"when">> => #{
        %% Activate for uncommitted requests only
        <<"committers">> => <<"uncommitted">>,
        
        %% Activate when authorization header present
        <<"keys">> => [<<"authorization">>]
    }
}
ConditionValueMeaning
committers<<"always">>All requests
committers<<"uncommitted">>Only unsigned (default)
committers[Addr1, Addr2]Only if signed by listed
keys<<"always">>Regardless of keys
keys[Key1, Key2]If any key present

Ignored Keys

These keys are excluded from signatures:

  • <<"secret">>, <<"cookie">>, <<"set-cookie">>
  • <<"path">>, <<"method">>, <<"authorization">>
  • <<"!">> (commit marker)

Part 2: HTTP Basic Authentication

📖 Reference: dev_codec_http_auth

dev_codec_http_auth implements HTTP Basic authentication with PBKDF2 key derivation for secure password-based signing.

Authentication Flow

┌─────────────────────────────────┐
│ Browser sends request (no auth) │
└────────────────┬────────────────┘

┌─────────────────────────────────┐
│ Server returns 401 Unauthorized │
│ WWW-Authenticate: Basic         │
└────────────────┬────────────────┘

┌─────────────────────────────────┐
│ Browser shows login prompt      │
│ Username: [________]            │
│ Password: [________]            │
└────────────────┬────────────────┘

┌─────────────────────────────────┐
│ Browser resends with:           │
│ Authorization: Basic <base64>   │
└────────────────┬────────────────┘

┌─────────────────────────────────┐
│ Server derives key via PBKDF2   │
│ Signs message with HMAC-SHA256  │
└─────────────────────────────────┘

Using HTTP Auth

%% Generate key from credentials
Credentials = base64:encode(<<"username:password">>),
Req = #{<<"authorization">> => <<"Basic ", Credentials/binary>>},
 
{ok, Key} = dev_codec_http_auth:generate(#{}, Req, #{}).
%% Key is PBKDF2-derived from credentials
 
%% Sign a message
Base = #{<<"data">> => <<"important message">>},
{ok, Signed} = dev_codec_http_auth:commit(Base, Req, #{}).

PBKDF2 Parameters

ParameterDefaultDescription
saltSHA256("constant
")
Public constant for reproducibility
iterations1,200,0002× OWASP 2023 recommendation
algSHA-256Hash algorithm
key-length64 bytesOutput key size

Custom Parameters

CustomReq = #{
    <<"authorization">> => <<"Basic ", Credentials/binary>>,
    <<"salt">> => <<"custom-salt">>,
    <<"iterations">> => 500000,
    <<"alg">> => <<"sha512">>,
    <<"key-length">> => 32
},
{ok, CustomKey} = dev_codec_http_auth:generate(Base, CustomReq, #{}).

Error Responses

%% No auth header → 401 Unauthorized
{error, #{
    <<"status">> => 401,
    <<"www-authenticate">> => <<"Basic">>,
    <<"details">> => <<"No authorization header provided.">>
}}
 
%% Invalid scheme → 400 Bad Request
{error, #{
    <<"status">> => 400,
    <<"details">> => <<"Unrecognized authorization header: Bearer token123">>
}}

Part 3: Cookie-Based Authentication

📖 Reference: dev_codec_cookie_auth

dev_codec_cookie_auth stores secrets in HTTP cookies for persistent session-based authentication.

How Cookie Auth Works

First Request (No Cookie):
Request (no cookies)


    Generate new secret


    Sign with secret


    Store secret in cookie


    Add Set-Cookie header


Response (with Set-Cookie)
Subsequent Requests:
Request (with cookie)


    Extract secret from cookie


    Sign with found secret


Response

Using Cookie Auth

%% First commit (generates new secret)
Base = #{<<"data">> => <<"test">>},
{ok, Signed} = dev_codec_cookie_auth:commit(Base, #{}, #{}),
 
%% Extract cookies for next request
{ok, Cookies} = dev_codec_cookie:extract(Signed, #{}, #{}),
 
%% Use cookies in next request
{ok, Req2} = dev_codec_cookie:store(#{}, Cookies, #{}),
{ok, Signed2} = dev_codec_cookie_auth:commit(Base2, Req2, #{}).

Cookie Secret Storage

Secrets are stored with hash-based keys:

%% Secret stored as:
Secret = crypto:strong_rand_bytes(64),
SecretHash = crypto:hash(sha256, Secret),
CookieKey = <<"secret-", (hb_util:encode(SecretHash))/binary>>.
%% Cookie: secret-e3QtMz...=<base64-encoded-secret>

Generator Interface

For use with auth hook:

%% generate/3 - Creates or retrieves secret
{ok, RequestWithSecret} = dev_codec_cookie_auth:generate(#{}, Request, #{}).
%% Returns request with <<"secret">> key
 
%% finalize/3 - Adds Set-Cookie to response
{ok, FinalSequence} = dev_codec_cookie_auth:finalize(#{}, HookReq, #{}).
%% Appends set-cookie message to sequence

Part 4: Cookie Management

📖 Reference: dev_codec_cookie

dev_codec_cookie handles parsing, encoding, and storage of HTTP cookies.

Parsing Cookies

%% Parse Cookie header
Msg = #{<<"cookie">> => <<"session=abc123; user=john; theme=dark">>},
{ok, Parsed} = dev_codec_cookie:from(Msg, #{}, #{}),
{ok, Cookies} = dev_codec_cookie:extract(Parsed, #{}, #{}).
%% => #{<<"session">> => <<"abc123">>,
%%      <<"user">> => <<"john">>,
%%      <<"theme">> => <<"dark">>}
 
%% Parse Set-Cookie header
Msg = #{<<"set-cookie">> => [<<"session=abc123; Path=/; HttpOnly">>]},
{ok, Cookies} = dev_codec_cookie:extract(Msg, #{}, #{}).
%% => #{<<"session">> => #{
%%        <<"value">> => <<"abc123">>,
%%        <<"attributes">> => #{<<"Path">> => <<"/">>},
%%        <<"flags">> => [<<"HttpOnly">>]
%%     }}

Storing Cookies

%% Store cookies in message
Base = #{},
Req = #{
    <<"session">> => <<"abc123">>,
    <<"user">> => <<"john">>
},
{ok, Updated} = dev_codec_cookie:store(Base, Req, #{}).
 
%% Get specific cookie
{ok, Session} = dev_codec_cookie:get_cookie(
    Updated,
    #{<<"key">> => <<"session">>},
    #{}
).

Format Conversion

%% Convert to Set-Cookie headers
{ok, WithSetCookie} = dev_codec_cookie:to(
    Msg,
    #{<<"format">> => <<"set-cookie">>},
    #{}
),
SetCookies = maps:get(<<"set-cookie">>, WithSetCookie).
%% => [<<"session=abc123; Path=/; HttpOnly">>]
 
%% Convert to Cookie header
{ok, WithCookie} = dev_codec_cookie:to(
    Msg,
    #{<<"format">> => <<"cookie">>},
    #{}
),
Cookie = maps:get(<<"cookie">>, WithCookie).
%% => <<"session=abc123; user=john">>

Cookie Attributes

AttributeDescription
PathURL path scope
DomainDomain scope
ExpiresExpiration date
Max-AgeLifetime in seconds
SameSiteCSRF protection
FlagDescription
SecureHTTPS only
HttpOnlyNo JavaScript access

Part 5: Secret Key Management

📖 Reference: dev_secret

dev_secret provides secure key management for trusted nodes, allowing generation, import, export, and signing with hosted secrets.

API Endpoints

PathMethodDescription
/generateGET/POSTGenerate new secret
/importPOSTImport existing secret
/listGETList hosted secrets
/commitPOSTSign with secret
/exportGETExport secret(s)
/syncGETSync from remote node

Generating Secrets

%% Generate in-memory secret
{ok, Response} = hb_http:get(
    Node,
    <<"/~secret@1.0/generate?persist=in-memory">>,
    #{}
),
KeyID = maps:get(<<"body">>, Response),
Address = maps:get(<<"wallet-address">>, Response).

Persistence Modes

ModeStorageKey ReturnedUse Case
clientNot storedYes (in cookie)User-held keys
in-memoryRAM onlyNoEphemeral secrets
non-volatileDiskNoPersistent secrets

Signing with Secrets

%% Generate a client secret
{ok, GenResp} = hb_http:get(
    Node,
    <<"/~secret@1.0/generate?persist=client">>,
    #{}
),
Priv = maps:get(<<"priv">>, GenResp),
 
%% Sign a message
{ok, Signed} = hb_http:post(
    Node,
    #{
        <<"device">> => <<"secret@1.0">>,
        <<"path">> => <<"commit">>,
        <<"body">> => <<"Important data">>,
        <<"priv">> => Priv
    },
    #{}
).

Importing and Exporting

%% Import existing wallet
TestWallet = ar_wallet:new(),
WalletKey = hb_escape:encode_quotes(ar_wallet:to_json(TestWallet)),
{ok, _} = hb_http:get(
    Node,
    <<"/~secret@1.0/import?persist=in-memory&key=", WalletKey/binary>>,
    #{}
).
 
%% Export secrets (requires admin)
{ok, Exported} = hb_http:get(
    Node,
    (hb_message:commit(#{
        <<"keyids">> => <<"all">>
    }, AdminOpts))#{<<"path">> => <<"/~secret@1.0/export">>},
    #{}
).

Try It: Complete Authentication Examples

%%% File: test_dev8.erl
-module(test_dev8).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
%% Run with: rebar3 eunit --module=test_dev8
 
auth_hook_exports_test() ->
    code:ensure_loaded(dev_auth_hook),
    ?assert(erlang:function_exported(dev_auth_hook, request, 3)),
    ?debugFmt("Auth hook exports: OK", []).
 
http_auth_generate_test() ->
    Credentials = base64:encode(<<"user:password">>),
    Req = #{<<"authorization">> => <<"Basic ", Credentials/binary>>},
    {ok, Key} = dev_codec_http_auth:generate(#{}, Req, #{}),
    ?assert(is_binary(Key)),
    ?assert(byte_size(Key) > 0),
    ?debugFmt("HTTP auth generate: OK (key size ~p)", [byte_size(Key)]).
 
http_auth_no_header_test() ->
    Result = dev_codec_http_auth:generate(#{}, #{}, #{}),
    ?assertMatch(
        {error, #{<<"status">> := 401, <<"www-authenticate">> := <<"Basic">>}},
        Result
    ),
    ?debugFmt("HTTP auth 401: OK", []).
 
http_auth_commit_test() ->
    Credentials = base64:encode(<<"user:password">>),
    Base = #{<<"data">> => <<"test message">>},
    Req = #{<<"authorization">> => <<"Basic ", Credentials/binary>>},
    {ok, Signed} = dev_codec_http_auth:commit(Base, Req, #{}),
    ?assert(maps:is_key(<<"commitments">>, Signed)),
    ?debugFmt("HTTP auth commit: OK", []).
 
cookie_auth_commit_test() ->
    Base = #{<<"data">> => <<"test">>},
    {ok, Signed} = dev_codec_cookie_auth:commit(Base, #{}, #{}),
    ?assert(maps:is_key(<<"commitments">>, Signed)),
    Commitments = maps:get(<<"commitments">>, Signed),
    ?assertEqual(1, map_size(Commitments)),
    ?debugFmt("Cookie auth commit: OK", []).
 
cookie_auth_generate_test() ->
    {ok, Result} = dev_codec_cookie_auth:generate(#{}, #{}, #{}),
    ?assert(maps:is_key(<<"secret">>, Result)),
    Secrets = maps:get(<<"secret">>, Result),
    ?assert(is_list(Secrets)),
    ?assertEqual(1, length(Secrets)),
    ?debugFmt("Cookie auth generate: OK", []).
 
cookie_parse_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)),
    ?debugFmt("Cookie parse: OK", []).
 
cookie_store_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)),
    ?debugFmt("Cookie store: OK", []).
 
cookie_get_test() ->
    Opts = hb_private:opts(#{}),
    Base = hb_private:set(#{}, <<"cookie">>, #{
        <<"session">> => <<"abc123">>
    }, Opts),
    Req = #{<<"key">> => <<"session">>},
    {ok, Cookie} = dev_codec_cookie:get_cookie(Base, Req, #{}),
    ?assertEqual(<<"abc123">>, Cookie),
    ?debugFmt("Cookie get: OK", []).
 
secret_exports_test() ->
    code:ensure_loaded(dev_secret),
    ?assert(erlang:function_exported(dev_secret, generate, 3)),
    ?assert(erlang:function_exported(dev_secret, import, 3)),
    ?assert(erlang:function_exported(dev_secret, list, 3)),
    ?assert(erlang:function_exported(dev_secret, commit, 3)),
    ?assert(erlang:function_exported(dev_secret, export, 3)),
    ?debugFmt("Secret device exports: OK", []).
 
complete_auth_workflow_test() ->
    ?debugFmt("=== Complete Auth Workflow ===", []),
    
    %% 1. HTTP Basic auth flow
    Credentials = base64:encode(<<"alice:secret123">>),
    AuthReq = #{<<"authorization">> => <<"Basic ", Credentials/binary>>},
    {ok, Key} = dev_codec_http_auth:generate(#{}, AuthReq, #{}),
    ?assert(is_binary(Key)),
    ?debugFmt("1. HTTP auth key derived", []),
    
    %% 2. Sign message with HTTP auth
    Message = #{<<"action">> => <<"transfer">>, <<"amount">> => 100},
    {ok, SignedHttp} = dev_codec_http_auth:commit(Message, AuthReq, #{}),
    ?assert(maps:is_key(<<"commitments">>, SignedHttp)),
    ?debugFmt("2. Message signed with HTTP auth", []),
    
    %% 3. Cookie auth flow
    {ok, SignedCookie} = dev_codec_cookie_auth:commit(Message, #{}, #{}),
    ?assert(maps:is_key(<<"commitments">>, SignedCookie)),
    ?debugFmt("3. Message signed with cookie auth", []),
    
    %% 4. Extract and reuse cookie
    {ok, Cookies} = dev_codec_cookie:extract(SignedCookie, #{}, #{}),
    SecretKeys = [K || <<"secret-", _/binary>> = K <- maps:keys(Cookies)],
    ?assert(length(SecretKeys) > 0),
    ?debugFmt("4. Cookie extracted for reuse", []),
    
    ?debugFmt("=== All tests passed! ===", []).

Run the Tests

rebar3 eunit --module=test_dev8

Common Patterns

Pattern 1: Cookie Session Authentication

%% Node setup with cookie-based sessions
Node = hb_http_server:start_node(#{
    priv_wallet => ar_wallet:new(),
    on => #{
        <<"request">> => #{
            <<"device">> => <<"auth-hook@1.0">>,
            <<"path">> => <<"request">>,
            <<"secret-provider">> => #{
                <<"device">> => <<"cookie@1.0">>
            }
        }
    }
}).
 
%% Client makes requests - cookies handled automatically
{ok, Resp} = hb_http:get(Node, <<"/protected-resource">>, #{}).
%% First request gets Set-Cookie, subsequent use that cookie

Pattern 2: Username/Password Authentication

%% Node setup with HTTP Basic auth
Node = hb_http_server:start_node(#{
    priv_wallet => ar_wallet:new(),
    on => #{
        <<"request">> => #{
            <<"device">> => <<"auth-hook@1.0">>,
            <<"path">> => <<"request">>,
            <<"when">> => #{
                <<"keys">> => [<<"authorization">>]
            },
            <<"secret-provider">> => #{
                <<"device">> => <<"http-auth@1.0">>
            }
        }
    }
}).
 
%% Client provides credentials
Creds = base64:encode(<<"user:pass">>),
Request = #{
    <<"path">> => <<"/api/action">>,
    <<"authorization">> => <<"Basic ", Creds/binary>>
},
{ok, Resp} = hb_http:post(Node, Request, #{}).

Pattern 3: Managed Key Vault

%% Generate and manage secrets on trusted node
Node = hb_http_server:start_node(#{
    priv_wallet => AdminWallet
}),
 
%% Generate a new secret
{ok, Gen} = hb_http:get(
    Node,
    <<"/~secret@1.0/generate?persist=non-volatile">>,
    #{}
),
KeyID = maps:get(<<"body">>, Gen),
 
%% Use it to sign messages
{ok, Signed} = hb_http:post(
    Node,
    #{
        <<"path">> => <<"/~secret@1.0/commit">>,
        <<"keyid">> => KeyID,
        <<"body">> => DataToSign
    },
    #{}
).

Pattern 4: Multi-Node Key Sync

%% Sync secrets between nodes
{ok, _} = hb_http:get(
    LocalNode,
    <<"/~secret@1.0/sync?node=", RemoteNode/binary, "&wallets=all">>,
    #{}
).

Quick Reference Card

📖 Reference: dev_auth_hook | dev_codec_http_auth | dev_codec_cookie_auth | dev_secret

%% === AUTH HOOK ===
%% Configure on node
on => #{
    <<"request">> => #{
        <<"device">> => <<"auth-hook@1.0">>,
        <<"secret-provider">> => #{<<"device">> => <<"cookie@1.0">>}
    }
}
 
%% Activation conditions
<<"when">> => #{
    <<"committers">> => <<"uncommitted">>,
    <<"keys">> => [<<"authorization">>]
}
 
%% === HTTP BASIC AUTH ===
%% Generate key from credentials
Creds = base64:encode(<<"user:pass">>),
Req = #{<<"authorization">> => <<"Basic ", Creds/binary>>},
{ok, Key} = dev_codec_http_auth:generate(#{}, Req, #{}).
 
%% Sign message
{ok, Signed} = dev_codec_http_auth:commit(Base, Req, #{}).
 
%% === COOKIE AUTH ===
%% Sign with auto-generated secret
{ok, Signed} = dev_codec_cookie_auth:commit(Base, #{}, #{}).
 
%% Generate secret for hook
{ok, ReqWithSecret} = dev_codec_cookie_auth:generate(#{}, Req, #{}).
 
%% === COOKIE MANAGEMENT ===
%% Parse cookies
{ok, Cookies} = dev_codec_cookie:extract(Msg, #{}, #{}).
 
%% Store cookies
{ok, Updated} = dev_codec_cookie:store(Base, Cookies, #{}).
 
%% Get specific cookie
{ok, Value} = dev_codec_cookie:get_cookie(Base, #{<<"key">> => Name}, #{}).
 
%% === SECRET DEVICE ===
%% Generate
{ok, _} = hb_http:get(Node, <<"/~secret@1.0/generate?persist=in-memory">>, #{}).
 
%% List
{ok, List} = hb_http:get(Node, <<"/~secret@1.0/list">>, #{}).
 
%% Commit (sign)
{ok, Signed} = hb_http:post(Node, #{
    <<"path">> => <<"/~secret@1.0/commit">>,
    <<"keyid">> => KeyID,
    <<"body">> => Data
}, #{}).
 
%% Export
{ok, Exported} = hb_http:get(Node, SignedReq#{
    <<"path">> => <<"/~secret@1.0/export">>
}, #{}).

Security Considerations

Trust Requirements

  1. Node Operator Trust — Users must trust the node operator
  2. Environment Security — Consider TEE or isolated deployment
  3. Secret Management — Secrets must be securely generated/stored
  4. Wallet Determinism — Same secret always produces same wallet

Best Practices

  1. Use TEEs — Deploy in Trusted Execution Environments when possible
  2. Limit Scope — Use when conditions to limit signing scope
  3. Monitor Usage — Log all signing operations for audit
  4. Rotate Secrets — Implement secret rotation policies
  5. HTTPS Only — Always use TLS for credential transmission

What's Next?

You now understand the authentication layer:

DevicePurposeMethod
dev_auth_hookRequest signing gatewayHook
dev_codec_http_authPassword authenticationPBKDF2 + HMAC
dev_codec_cookie_authSession authenticationCookie + HMAC
dev_codec_cookieCookie managementParse/Store
dev_secretKey vaultGenerate/Sign

Going Further

  1. Arweave & Data — Permanent storage (Tutorial)

Resources

HyperBEAM Documentation

Related Tutorials