Authentication — User-Scoped Sessions

What this enables:

  • Tool calls attributed to a specific end user, not just the project
  • Member identity (X-Tengine-Member-Id) injected into every custom tool call
  • Multi-tenant isolation — each user's session is independent

User-scoped sessions let you attribute each MCP tool call to a specific end user. Instead of a single project API key that authenticates all requests, your backend mints a short-lived member session token for each user, which is then passed to the Anthropic SDK as the authorization_token.

Use this mode when:

  • Your product is multi-tenant and each user has their own data
  • You need tool calls attributed to individual users
  • Tools should operate in the context of a specific user (e.g., "look up Alice's records, not the project's")

How It Works

Your Backend                    TengineAI
─────────────────────────────────────────────────────
1. Generate key pair (one-time setup)
2. Register public key  ──────────────────────────▶  Store public key (kid)
3. Per request:
   Sign member_assertion JWT with private key
4. Exchange for session token ────────────────────▶  Verify signature
                              ◀────────────────────  Return tng_mst_... token
5. Pass token to Anthropic SDK
   mcp_servers[authorization_token]  ─────────────▶  Authenticate MCP session
                                                      as that specific member

The session token carries the member's identity inside TengineAI — tools run in the context of that user for the duration of the session.


Step 1: Generate a Key Pair

Generate an EC key pair using the P-256 curve (ES256). This is a one-time setup per environment.

# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem

# Extract public key
openssl ec -in private_key.pem -pubout -out public_key.pem

Keep private_key.pem secret. It lives on your backend only and should never be committed to source control or sent to TengineAI.

Supported algorithms: ES256 (recommended), ES384, Ed25519, RS256.


Step 2: Register Your Public Key

Register the public key with TengineAI using your project API key. The kid (key ID) is a string you choose — use something that lets you identify and rotate keys later.

import os
import requests

headers = {"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"}

with open("public_key.pem") as f:
    public_key_pem = f.read()

response = requests.post(
    "https://app.tengine.ai/api/v1/assertion-keys",
    headers=headers,
    json={
        "kid": "my-backend-v1",
        "public_key_pem": public_key_pem,
        "algorithm": "ES256",
        "description": "Production signing key",
    },
)
response.raise_for_status()
print(response.json())

You only need to do this once per key. Store the kid — you'll reference it in every member_assertion JWT you sign.


Step 3: Mint a member_assertion JWT

Security requirements — read before continuing:

  • The member_assertion JWT must have a short TTL (≤ 60 seconds). It is a one-time-use proof of identity, not a session credential. If intercepted, a long-lived assertion can be replayed.
  • The member_assertion is not the same as the session token. The assertion proves identity to TengineAI. The session token (tng_mst_...) is what you pass to the Anthropic SDK.
  • Never send your private key to the frontend. Assertion signing must happen server-side only. If a client ever touches the private key, your entire identity system is compromised.

For each user session, your backend signs a short-lived JWT asserting the user's identity. TengineAI verifies this signature against the registered public key before issuing a session token.

Required claims:

ClaimTypeDescription
substringYour internal user ID (any stable identifier)
audstringMust be tengine:project:<your_project_id>
expintUnix timestamp — keep TTL short (30–120 seconds)
iatintUnix timestamp of issuance

Optional claims:

ClaimTypeDescription
emailstringUser email — forwarded in member context
roleslistUser roles — forwarded in member context

JWT header must include kid matching the key registered in Step 2.

import os
import time
import jwt  # pip install PyJWT cryptography

with open("private_key.pem") as f:
    private_key = f.read()

PROJECT_ID = os.environ["TENGINEAI_PROJECT_ID"]

def mint_member_assertion(user_id: str, email: str = None) -> str:
    now = int(time.time())
    payload = {
        "sub": user_id,
        "aud": f"tengine:project:{PROJECT_ID}",
        "iat": now,
        "exp": now + 60,  # 60-second TTL — used only for the exchange
    }
    if email:
        payload["email"] = email

    return jwt.encode(
        payload,
        private_key,
        algorithm="ES256",
        headers={"kid": "my-backend-v1"},
    )

The member_assertion is ephemeral — it exists only to prove identity to TengineAI during the token exchange. It is never stored.


Step 4: Exchange for a TengineAI Session Token

Send the member_assertion to TengineAI's session token endpoint along with your project API key. TengineAI verifies the assertion signature and returns a tng_mst_... session token.

import os
import requests

def get_member_session_token(user_id: str, email: str = None) -> dict:
    assertion = mint_member_assertion(user_id, email)

    response = requests.post(
        "https://app.tengine.ai/api/v1/mcp/session-token",
        headers={"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"},
        json={"member_assertion": assertion},
    )
    response.raise_for_status()
    return response.json()
    # Returns: {"token": "tng_mst_...", "expires_in_minutes": 15, "token_type": "Bearer"}

The returned token has a 15-minute TTL by default. Cache it and reuse it for the duration of a user's session rather than minting a new one on every request.


Step 5: Use the Session Token with the Anthropic SDK

Pass token as the authorization_token in mcp_servers:

import json
import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

session = get_member_session_token(user_id="user_123", email="alice@example.com")

response = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Look up contact c_001 in the CRM."}],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://app.tengine.ai/mcp/",
            "name": "tengineai",
            "authorization_token": session["token"],
        }
    ],
    betas=["mcp-client-2025-04-04"],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "mcp_tool_use":
        print(
            f"[tool_use] server={block.server_name} tool={block.name} id={block.id} "
            f"input={json.dumps(block.input, default=str)}"
        )
    elif block.type == "mcp_tool_result":
        print(
            f"[tool_result] id={block.tool_use_id} is_error={getattr(block, 'is_error', False)}"
        )
        print(block.content)

TengineAI reads the session token, resolves the member identity, and executes tools in that user's context.


Session Token Reference

PropertyValue
Prefixtng_mst_
Default TTL15 minutes
AlgorithmHS256 (signed by TengineAI)
Required claimsiss, aud, sub, exp, iat, jti, kid
sub formatmember:<member_external_id>

The session token is opaque to your application — treat it as a bearer credential and do not attempt to decode it.


Key Rotation

Register a new key with a different kid, update your signing code to use the new key, then deactivate the old key via the API:

# Deactivate old key by ID
requests.delete(
    "https://app.tengine.ai/api/v1/assertion-keys/<key_id>",
    headers={"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"},
)

TengineAI validates against all active keys for your project, so you can rotate without downtime by activating the new key before deactivating the old one.


Next Steps