UserHero Docs
Embedded Portal

HMAC signing

Server-side examples for signing the Embedded Portal identity payload.

HMAC signing

UserHero verifies your identity payload with an HMAC-SHA256 signature computed over the JSON-encoded payload using your project's HMAC secret.

Only sign on your server. Never embed the HMAC secret in your frontend code, mobile binary, or any client-side environment. Treat it like a password.

Algorithm

  1. Build the identity payload as a JSON object.
  2. Serialize it to a string with JSON.stringify (or your language's equivalent — keys do not need to be sorted).
  3. Compute HMAC-SHA256(secret, jsonString) and encode the result as lowercase hex.
  4. Send the JSON string and the hex digest to the browser. The SDK passes both to UserHero.

Node.js

import crypto from 'node:crypto';

function signPayload(payload) {
    const json = JSON.stringify(payload);
    const hmac = crypto
        .createHmac('sha256', process.env.USERHERO_HMAC_SECRET)
        .update(json)
        .digest('hex');
    return { json, hmac };
}

const { json, hmac } = signPayload({
    externalUserId: user.id,
    email: user.email,
    expiresAt: Math.floor(Date.now() / 1000) + 300,
});

// Render to your page:
// <script>window.__UH_USER__ = JSON.parse({{ json|safe }}); window.__UH_HMAC__ = "{{ hmac }}";</script>

Python

import hmac, hashlib, json, os, time

def sign_payload(payload: dict) -> tuple[str, str]:
    secret = os.environ["USERHERO_HMAC_SECRET"].encode()
    body = json.dumps(payload, separators=(",", ":"))
    digest = hmac.new(secret, body.encode(), hashlib.sha256).hexdigest()
    return body, digest

body, digest = sign_payload({
    "externalUserId": user.id,
    "email": user.email,
    "expiresAt": int(time.time()) + 300,
})

Ruby

require 'openssl'
require 'json'

def sign_payload(payload)
    body = payload.to_json
    digest = OpenSSL::HMAC.hexdigest('SHA256', ENV['USERHERO_HMAC_SECRET'], body)
    [body, digest]
end

body, digest = sign_payload(
    externalUserId: user.id,
    email: user.email,
    expiresAt: Time.now.to_i + 300
)

PHP

<?php
function sign_payload(array $payload): array {
    $body = json_encode($payload, JSON_UNESCAPED_SLASHES);
    $digest = hash_hmac('sha256', $body, getenv('USERHERO_HMAC_SECRET'));
    return [$body, $digest];
}

[$body, $digest] = sign_payload([
    'externalUserId' => $user->id,
    'email' => $user->email,
    'expiresAt' => time() + 300,
]);

Go

package userhero

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "os"
    "time"
)

func SignPayload(payload map[string]any) (string, string, error) {
    body, err := json.Marshal(payload)
    if err != nil {
        return "", "", err
    }
    mac := hmac.New(sha256.New, []byte(os.Getenv("USERHERO_HMAC_SECRET")))
    mac.Write(body)
    return string(body), hex.EncodeToString(mac.Sum(nil)), nil
}

body, digest, _ := SignPayload(map[string]any{
    "externalUserId": user.ID,
    "email": user.Email,
    "expiresAt": time.Now().Unix() + 300,
})

Rotating the secret

You can rotate the HMAC secret at any time from Settings → Embedded Portal → Rotate. After rotation:

  • Old signatures stop working immediately.
  • The new secret is shown once — copy it to your secrets manager and redeploy.
  • Issue rotation outside peak traffic so existing portal sessions can re-authenticate quickly.

Rotate immediately if you suspect the secret has leaked.

On this page