Skip to content

Authentication

The Aster Policy Engine enforces a three-layer security model on every request. Each layer is independent; all three must be satisfied for a request to succeed.

Layer 1 — Tenant Isolation (X-Tenant-Id)

Every request must carry an X-Tenant-Id header. This header establishes the tenant context for the request: policies, audit records, and all data are scoped to the specified tenant and are never visible to other tenants.

Format: ^[a-zA-Z0-9_-]{1,64}$

A missing or malformed X-Tenant-Id returns 400 Bad Request immediately, before any other validation is performed.

bash
curl https://policy.aster-lang.dev/api/v1/policies \
  -H "X-Tenant-Id: acme-corp"

Tenant ID conventions

Use a stable, human-readable identifier such as your organisation slug or environment name (e.g. acme-corp, acme-corp-staging). Tenant IDs are case-sensitive.

Layer 2 — HMAC Request Signing

Every request must include HMAC signature headers. The server validates the signature before processing the request, preventing tampering in transit and protecting against replay attacks.

Required Headers

HeaderDescription
X-Aster-SignatureHex-encoded HMAC-SHA256 of the canonical message (see below)
X-Aster-NonceA randomly-generated string (recommend 16+ bytes, hex or UUID)
X-Aster-TimestampUnix timestamp in milliseconds at the moment of signing

A request missing any of these three headers returns 401 Unauthorized.

Canonical Message Format

The HMAC is computed over the following pipe-delimited string:

{HTTP-method}|{path}|{query}|{X-Aster-Timestamp}|{X-Aster-Nonce}|{body-sha256}
ComponentDescriptionExample
HTTP-methodUppercase HTTP methodPOST
pathRequest URI path/api/v1/policies/evaluate-source
queryRaw query string (empty string if none)trace=true
X-Aster-TimestampTimestamp value from header1708776000000
X-Aster-NonceNonce value from headerc3ab8ff13720e8ad9047dd39466b3c89
body-sha256Lowercase hex SHA-256 hash of the raw request bodya1b2c3d4...

The HMAC key is the API secret issued to your tenant. The secret is resolved in priority order:

  1. Environment variable ASTER_HMAC_SECRET_{TENANT_ID} (tenant ID uppercased, dashes replaced with underscores)
  2. Configuration property aster.security.hmac.secret-key

Example: Computing the Signature (Bash)

bash
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module demo.\n\nRule ping produce Text:\n  Return \"pong\".","functionName":"ping","context":{},"locale":"en-US"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"
QUERY=""

BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
CANONICAL="${METHOD}|${PATH_URI}|${QUERY}|${TIMESTAMP}|${NONCE}|${BODY_HASH}"
SIGNATURE=$(printf '%s' "${CANONICAL}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')

curl -X POST "https://policy.aster-lang.dev${PATH_URI}" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: ${TENANT_ID}" \
  -H "X-User-Role: MEMBER" \
  -H "X-Aster-Signature: ${SIGNATURE}" \
  -H "X-Aster-Nonce: ${NONCE}" \
  -H "X-Aster-Timestamp: ${TIMESTAMP}" \
  -d "${BODY}"

Replay Prevention

The server rejects requests where X-Aster-Timestamp is more than 5 minutes (300,000 milliseconds) in the past or future, returning 401 Unauthorized. Additionally, each nonce is stored for the duration of the replay window; a second request using the same nonce within the window is rejected with 409 Conflict.

Layer 3 — Role-Based Access Control (X-User-Role)

The X-User-Role header carries the caller's role claim. The server enforces a strict role hierarchy:

OWNER > ADMIN > MEMBER > VIEWER

Each role is additive: a higher role inherits all permissions of roles below it.

Role Permissions

RolePermitted actions
VIEWERRead stored policies (GET)
MEMBERAll VIEWER permissions + evaluate policies (POST to evaluation endpoints)
ADMINAll MEMBER permissions + read and verify audit logs
OWNERAll ADMIN permissions + manage tenant settings and RBAC assignments

Endpoint Requirements

Endpoint categoryMinimum required role
Policy evaluationMEMBER
Policy management (CRUD)MEMBER
Audit log readADMIN
Audit log verificationADMIN
Tenant administrationOWNER

A request with an insufficient role returns 403 Forbidden.

Optional Headers

HeaderDescriptionDefault
X-User-IdIdentifies the caller in audit logsanonymous

Example: Full Request with All Layers

bash
TENANT_ID="acme-corp"
TIMESTAMP=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY='{"source":"Module demo.\n\nRule ping produce Text:\n  Return \"pong\".","functionName":"ping","context":{},"locale":"en-US"}'
API_SECRET="your-api-secret-here"
METHOD="POST"
PATH_URI="/api/v1/policies/evaluate-source"

BODY_HASH=$(printf '%s' "${BODY}" | openssl dgst -sha256 | awk '{print $2}')
SIGNATURE=$(printf '%s' "${METHOD}|${PATH_URI}||${TIMESTAMP}|${NONCE}|${BODY_HASH}" | openssl dgst -sha256 -hmac "${API_SECRET}" | awk '{print $2}')

curl -X POST "https://policy.aster-lang.dev${PATH_URI}" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: ${TENANT_ID}" \
  -H "X-User-Id: user@acme.com" \
  -H "X-User-Role: MEMBER" \
  -H "X-Aster-Signature: ${SIGNATURE}" \
  -H "X-Aster-Nonce: ${NONCE}" \
  -H "X-Aster-Timestamp: ${TIMESTAMP}" \
  -d "${BODY}"

Summary

LayerHeader(s)RequiredFailure response
1X-Tenant-IdAlways400 Bad Request
2X-Aster-Signature, X-Aster-Nonce, X-Aster-TimestampAlways401 Unauthorized
3X-User-RoleAlways403 Forbidden
X-User-IdOptionalDefaults to anonymous

Released under the MIT License.