Skip to content

Per-page sharing

How a target app derives the per-page token from the share seed and serves the shared page — with stdlib-only examples in Go, Ruby, JavaScript, and Python.

You get the three inputs from the page-share reveal modal — in the cloud console's sessions page or the gateway's local sessions page.

How it works

The gateway mints a page-share key for your app: a base token of shape nh.sid.ts.mac (opaque — never parse it beyond splitting at the last dot) and a 32-byte share seed (base64url, revealed once). The reveal modal shows three values: the public host, the base token, and the seed. From these your app derives one short token per page and builds a public link to exactly that page — no other page is reachable through it.

The security model: the gateway is the sole verifier — your app only derives tokens, it never validates them. The seed stays server-side in your app and is never sent to a browser. Revoking the base session at the gateway kills every per-page link minted from it at once.

Derivation

Page token (22 chars)b64url_nopad( HMAC-SHA256(key=seed, msg="page\n" + host + "\n" + path)[:16] )
Share URLhttps://<host><path>?<mac>=<nh.sid.ts>&<page_token>=p
  • path excludes any query string.
  • The base token splits at its last dot: the trailing mac becomes the first query param's key, the rest (nh.sid.ts) its value. A base token without a dot is invalid — reject it.
  • The derived page token is the second param's key, with the literal value p.
  • The seed is base64url (accept padded or unpadded) and must decode to exactly 32 bytes.

Examples

Stdlib-only, no dependencies. Each snippet prints the share URL for the test vector below.

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"
)

// pageShareToken derives the 22-char per-page token:
// b64url_nopad( HMAC-SHA256(seed, "page\n"+host+"\n"+path)[:16] )
func pageShareToken(seed []byte, host, path string) string {
	mac := hmac.New(sha256.New, seed)
	mac.Write([]byte("page\n" + host + "\n" + path))
	return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)[:16])
}

// pageShareURL assembles https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
func pageShareURL(seedB64, host, baseToken, path string) (string, error) {
	seed, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(seedB64, "="))
	if err != nil {
		return "", fmt.Errorf("seed is not base64url: %w", err)
	}
	if len(seed) != 32 {
		return "", fmt.Errorf("seed decodes to %d bytes, want 32", len(seed))
	}
	i := strings.LastIndexByte(baseToken, '.')
	if i <= 0 || i == len(baseToken)-1 {
		return "", errors.New("base token has no value.mac split")
	}
	value, key := baseToken[:i], baseToken[i+1:]
	return "https://" + host + path +
		"?" + key + "=" + value +
		"&" + pageShareToken(seed, host, path) + "=p", nil
}

func main() {
	url, err := pageShareURL(
		"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
		"example.com", "nh.sid.ts.mac", "/docs/example")
	if err != nil {
		panic(err)
	}
	fmt.Println(url)
}
ruby
require "openssl"
require "base64"

# b64url_nopad( HMAC-SHA256(seed, "page\n" + host + "\n" + path)[0,16] )
def page_share_token(seed, host, path)
  mac = OpenSSL::HMAC.digest("SHA256", seed, "page\n#{host}\n#{path}")
  Base64.urlsafe_encode64(mac[0, 16], padding: false)
end

# https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
def page_share_url(seed_b64, host, base_token, path)
  s = seed_b64.delete("=")
  seed = Base64.urlsafe_decode64(s + "=" * (-s.length % 4))
  raise "seed decodes to #{seed.bytesize} bytes, want 32" unless seed.bytesize == 32
  i = base_token.rindex(".")
  raise "base token has no value.mac split" if i.nil? || i.zero? || i == base_token.length - 1
  value, key = base_token[0...i], base_token[i + 1..]
  "https://#{host}#{path}?#{key}=#{value}&#{page_share_token(seed, host, path)}=p"
end

puts page_share_url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
                    "example.com", "nh.sid.ts.mac", "/docs/example")
js
const crypto = require("crypto");

// b64url_nopad( HMAC-SHA256(seed, "page\n" + host + "\n" + path).slice(0, 16) )
function pageShareToken(seed, host, path) {
  const mac = crypto.createHmac("sha256", seed)
    .update(`page\n${host}\n${path}`).digest();
  return mac.subarray(0, 16).toString("base64url");
}

// https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
function pageShareURL(seedB64, host, baseToken, path) {
  const seed = Buffer.from(seedB64.replace(/=+$/, ""), "base64url");
  if (seed.length !== 32) throw new Error(`seed decodes to ${seed.length} bytes, want 32`);
  const i = baseToken.lastIndexOf(".");
  if (i <= 0 || i === baseToken.length - 1) throw new Error("base token has no value.mac split");
  const value = baseToken.slice(0, i), key = baseToken.slice(i + 1);
  return `https://${host}${path}?${key}=${value}&${pageShareToken(seed, host, path)}=p`;
}

console.log(pageShareURL("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
  "example.com", "nh.sid.ts.mac", "/docs/example"));
python
import base64
import hashlib
import hmac

def page_share_token(seed: bytes, host: str, path: str) -> str:
    """b64url_nopad( HMAC-SHA256(seed, "page\\n" + host + "\\n" + path)[:16] )"""
    mac = hmac.new(seed, f"page\n{host}\n{path}".encode(), hashlib.sha256).digest()
    return base64.urlsafe_b64encode(mac[:16]).rstrip(b"=").decode()

def page_share_url(seed_b64: str, host: str, base_token: str, path: str) -> str:
    """https://<host><path>?<mac>=<nh.sid.ts>&<token>=p"""
    s = seed_b64.rstrip("=")
    seed = base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
    if len(seed) != 32:
        raise ValueError(f"seed decodes to {len(seed)} bytes, want 32")
    i = base_token.rfind(".")
    if i <= 0 or i == len(base_token) - 1:
        raise ValueError("base token has no value.mac split")
    value, key = base_token[:i], base_token[i + 1:]
    return f"https://{host}{path}?{key}={value}&{page_share_token(seed, host, path)}=p"

print(page_share_url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
                     "example.com", "nh.sid.ts.mac", "/docs/example"))

Test vector

Verify before going live

These inputs must produce exactly this page token:

seedAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8
hostexample.com
path/docs/example
token3jgF8OH9AxuQHTtySu-3BQ