> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noxus.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify Noxus identity tokens

> Know which person is chatting when a Noxus agent calls your MCP server

## What you get

When a workspace connects your MCP server with **Send who is chatting** turned on,
every request from Noxus to your server carries one extra header:

```
X-Noxus-Identity: eyJhbGciOiJFUzI1NiIsImtpZCI6Ii4uLiIsInR5cCI6IkpXVCJ9...
```

The value is a JSON Web Token (JWT) signed by Noxus for the person who is
chatting, or running the flow. Your server verifies the signature with the
public keys Noxus publishes, then reads who the person is. You do not call Noxus
per request, and you do not share a secret with Noxus.

Other headers you configured on the connection, such as your own API key, are
still sent as before.

## The values you need

Open the connection in Noxus (**Workspace control**, **Connections**, the
connection, **Identity token**). It shows every value below, ready to copy.

| Value | Example | Meaning |
| - | - | - |
| Header | `X-Noxus-Identity` | Where the token arrives |
| Public keys | `https://<noxus-backend>/.well-known/jwks.json` | JSON Web Key Set used to verify the signature |
| Issuer (`iss`) | `https://<noxus-backend>` | The Noxus deployment that signed the token |
| Audience (`aud`) | `1f3b9e2a-...` | The id of the Noxus workspace the call comes from |
| Algorithm | `ES256` | Elliptic curve P-256 signature |
| Token type (`token_use`) | `mcp_identity` | Tells this token apart from other Noxus tokens |
| Lifetime | 30 minutes | `exp` minus `iat` |

The token also carries `scope`: the tools it may call. It changes per call, so the
connection does not show it.

## The claims

```json theme={null}
{
  "iss": "https://<noxus-backend>",
  "aud": "1f3b9e2a-0000-0000-0000-000000000000",
  "token_use": "mcp_identity",
  "sub": "c7488eeb-8809-4399-85ff-1a9917cfd10e",
  "email": "ana.silva@example.com",
  "scope": "search_candidates get_candidate",
  "iat": 1790000000,
  "exp": 1790001800
}
```

`sub` is the Noxus user id. It is stable for the person. `email` is the email of
their Noxus account. Map either one to a user in your system.

`scope` lists the tools this token may call, separated by spaces. It holds the
tools the agent may use on your server (or the one tool an MCP Tool node calls),
minus the tools the workspace turned off. When Noxus only lists your tools, for
example when an admin presses **Refresh tools**, `scope` is empty.

## Checks your server must make

Reject the request (HTTP 401) unless every check passes:

1. The `X-Noxus-Identity` header is present.
2. The algorithm is `ES256`. Fix it in your verify call and accept nothing else.
3. The key id (`kid` in the token header) is in the published key set. If it is
   not, fetch the key set again once, then reject.
4. The signature verifies.
5. `iss` equals the issuer and `aud` is a workspace you allow. One Noxus
   deployment signs tokens for all of its workspaces, so the audience check is
   what makes the token yours.
6. The token has not expired. Allow about 60 seconds of clock skew.
7. `token_use` is `mcp_identity`.

Then map `sub` or `email` to your user, and return 403 if you do not know them.

For a tool call (`tools/call`), also return an error unless the tool name is in
`scope`. Listing tools (`tools/list`) does not need a scope.

A tool the server added after the last **Refresh tools** is not in `scope` until
an admin presses **Refresh tools** in Noxus.

Protect every MCP route. With the SSE transport that is both the stream (`GET
/sse`) and the message endpoint (`POST /messages`), because tool calls arrive on
the message endpoint.

Cache the key set. Fetch it on start, refresh it every few hours, and when a
token names a key id you have not seen. Do not fetch it per request.

## Example: TypeScript with jose

```ts theme={null}
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL("https://<noxus-backend>/.well-known/jwks.json"));
const ISSUER = "https://<noxus-backend>";
const ALLOWED_WORKSPACES = ["1f3b9e2a-0000-0000-0000-000000000000"];

export async function noxusIdentity(req, res, next) {
  const token = req.header("X-Noxus-Identity");
  if (!token) return res.status(401).json({ error: "missing identity token" });
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: ISSUER,
      audience: ALLOWED_WORKSPACES,
      algorithms: ["ES256"],
      clockTolerance: 60,
    });
    if (payload.token_use !== "mcp_identity") throw new Error("wrong token_use");
    req.scope = new Set(String(payload.scope).split(" "));
    req.user = await findUser({ noxusId: payload.sub, email: payload.email });
    if (!req.user) return res.status(403).json({ error: "unknown user" });
    return next();
  } catch (err) {
    return res.status(401).json({ error: String(err) });
  }
}
```

`jwtVerify` selects the key by `kid`, checks the signature, issuer, audience
(the workspace) and expiry. The `if` line checks the token type. Before a tool
runs, check that `req.scope` has its name.

## Example: Python with PyJWT

```python theme={null}
import jwt

jwks = jwt.PyJWKClient("https://<noxus-backend>/.well-known/jwks.json")
ALLOWED_WORKSPACES = ["1f3b9e2a-0000-0000-0000-000000000000"]

def noxus_identity(token: str) -> dict:
    key = jwks.get_signing_key_from_jwt(token).key
    claims = jwt.decode(
        token,
        key,
        algorithms=["ES256"],
        audience=ALLOWED_WORKSPACES,
        issuer="https://<noxus-backend>",
        leeway=60,
    )
    if claims["token_use"] != "mcp_identity":
        raise PermissionError("wrong token_use")
    return claims


def may_call(claims: dict, tool: str) -> bool:
    return tool in claims["scope"].split()
```

## When there is no token

Noxus sends the token only when a signed-in person is behind the call. Calls
from a workspace API key, a trigger, or a chat channel without a Noxus user stop
inside Noxus and never reach your server. The person sees why in the chat or in
the run error.

## Do not

* Trust an email or user header without a signature. Anyone who knows your URL
  can send one.
* Skip the audience or scope checks.
* Forward the token to another service. It names the workspace, not your server.
* Log the raw token. Log `sub`, `aud` and the result instead.
