Signing a Comms.ID request token for direct HTTP
Signing a Comms.ID request token for direct HTTP
Every hosted call carries Authorization: Bearer <token>: a short-lived EdDSA (Ed25519) JWT
signed with your app’s private key. The private key stays on your server; Comms.ID only ever
sees the public key you registered in the Companion App. The npm clients and
npx comms-id token <product> sign for you; this page is for any other language.
The token has the header {"alg":"EdDSA"} and these claims:
| Claim | Value |
|---|---|
iss |
your app id (COMMS_ID_CLIENT_ID) |
aud, scope |
the operation’s x-comms-id-auth in its OpenAPI document, for example Company: asic-registers, asic-registers:read |
sub, jti |
a new random UUID for each token |
iat, exp |
now, and now plus at most 120 seconds |
Make a new token for each request (or reuse one for at most its two minutes). Each example
reads COMMS_ID_CLIENT_ID and COMMS_ID_PRIVATE_JWK (the private JWK you generated) from the
environment and prints one token for the audience and scope you pass.
TOKEN=$(python3 sign.py asic-registers asic-registers:read)
curl -s https://api.comms.id/company/v1/lookup \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"acn":"004085616"}'
Use https://api-test.comms.id with a development app’s key for TEST (synthetic replies).
Python
"""Sign a Comms.ID request token (EdDSA JWT) with your app's private key.
Requires: pip install cryptography
Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
generated; it never leaves your server). Usage: python3 sign.py <audience> <scope>
Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.
"""
import base64, json, os, sys, time, uuid
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def token(client_id: str, private_jwk: dict, audience: str, scope: str, lifetime: int = 120) -> str:
seed = base64.urlsafe_b64decode(private_jwk["d"] + "=" * (-len(private_jwk["d"]) % 4))
key = Ed25519PrivateKey.from_private_bytes(seed)
now = int(time.time())
header = b64url(json.dumps({"alg": "EdDSA"}, separators=(",", ":")).encode())
claims = {"iss": client_id, "sub": str(uuid.uuid4()), "jti": str(uuid.uuid4()),
"aud": audience, "scope": scope, "iat": now, "exp": now + min(lifetime, 120)}
payload = b64url(json.dumps(claims, separators=(",", ":")).encode())
signing_input = f"{header}.{payload}".encode()
return f"{header}.{payload}.{b64url(key.sign(signing_input))}"
if __name__ == "__main__":
print(token(os.environ["COMMS_ID_CLIENT_ID"].strip(), json.loads(os.environ["COMMS_ID_PRIVATE_JWK"]), sys.argv[1], sys.argv[2]))
Go
// Sign a Comms.ID request token (EdDSA JWT) with your app's private key. Standard library only.
//
// Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
// generated; it never leaves your server). Usage: go run sign.go <audience> <scope>
// Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"time"
)
func uuid4() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
func Token(clientID string, privateJWK map[string]string, audience, scope string) (string, error) {
seed, err := base64.RawURLEncoding.DecodeString(privateJWK["d"])
if err != nil || len(seed) != ed25519.SeedSize {
return "", fmt.Errorf("COMMS_ID_PRIVATE_JWK is not an Ed25519 private key")
}
key := ed25519.NewKeyFromSeed(seed)
now := time.Now().Unix()
header, _ := json.Marshal(map[string]string{"alg": "EdDSA"})
claims, _ := json.Marshal(map[string]any{
"iss": clientID, "sub": uuid4(), "jti": uuid4(),
"aud": audience, "scope": scope, "iat": now, "exp": now + 120,
})
signingInput := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(claims)
return signingInput + "." + base64.RawURLEncoding.EncodeToString(ed25519.Sign(key, []byte(signingInput))), nil
}
func main() {
var jwk map[string]string
if err := json.Unmarshal([]byte(os.Getenv("COMMS_ID_PRIVATE_JWK")), &jwk); err != nil {
fmt.Fprintln(os.Stderr, "COMMS_ID_PRIVATE_JWK is not JSON")
os.Exit(1)
}
token, err := Token(strings.TrimSpace(os.Getenv("COMMS_ID_CLIENT_ID")), jwk, os.Args[1], os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(token)
}
PHP
<?php
// Sign a Comms.ID request token (EdDSA JWT) with your app's private key. Needs PHP's sodium
// extension (bundled since PHP 7.2).
//
// Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
// generated; it never leaves your server). Usage: php sign.php <audience> <scope>
// Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.
function comms_id_b64url(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function comms_id_uuid4(): string {
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
function comms_id_token(string $clientId, array $privateJwk, string $audience, string $scope): string {
$seed = base64_decode(strtr($privateJwk['d'], '-_', '+/'), true);
if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) {
throw new RuntimeException('COMMS_ID_PRIVATE_JWK is not an Ed25519 private key');
}
$secret = sodium_crypto_sign_secretkey(sodium_crypto_sign_seed_keypair($seed));
$now = time();
$header = comms_id_b64url(json_encode(['alg' => 'EdDSA']));
$payload = comms_id_b64url(json_encode([
'iss' => $clientId, 'sub' => comms_id_uuid4(), 'jti' => comms_id_uuid4(),
'aud' => $audience, 'scope' => $scope, 'iat' => $now, 'exp' => $now + 120,
]));
$signature = sodium_crypto_sign_detached("$header.$payload", $secret);
return "$header.$payload." . comms_id_b64url($signature);
}
if (PHP_SAPI === 'cli' && realpath($argv[0]) === __FILE__) {
echo comms_id_token(trim(getenv('COMMS_ID_CLIENT_ID')), json_decode(getenv('COMMS_ID_PRIVATE_JWK'), true), $argv[1], $argv[2]), "\n";
}
Each example is tested against the production verifier: its token is accepted, and a token
for another audience or with a changed signature is refused (platform
infra/signing-examples.test.ts).