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
- Build the identity payload as a JSON object.
- Serialize it to a string with
JSON.stringify(or your language's equivalent — keys do not need to be sorted). - Compute
HMAC-SHA256(secret, jsonString)and encode the result as lowercase hex. - 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.