# Backend Verification

When a MoonX-authenticated user calls your backend, the React SDK ships two (sometimes three) headers your server should verify before acting:

| Header | Token | Carries |
|---|---|---|
| `X-MoonX-Access-Token` | [Access token](/authentication/user-authentication/access-tokens) (~1h TTL) | `sub` (user id), `sid` (session id), audience, issuer, expiry |
| `X-MoonX-Identity-Token` | [Identity token](/authentication/user-authentication/identity-tokens) (~10h TTL) | OIDC profile claims: `email`, `name`, `picture`, etc. |
| `X-MoonX-Presence` | [Presence token](/authentication/user-authentication/presence-tokens) (~5m TTL) | Proof of a fresh passkey assertion. Only on high-risk routes. |

Tokens are signed with ES256 against your app's public JWKS. Verification runs entirely against the JWKS, with no shared secret between you and MoonX. The `@moon-x/node-sdk` package provides a zero-dependency client that handles this for you.

## Install

```bash
npm install @moon-x/node-sdk
```

The SDK uses only Web Crypto (`globalThis.crypto.subtle`) and `fetch`. It works in:

* Node 18+
* Cloudflare Workers
* Vercel Edge
* Bun, Deno

No JWT library, no additional dependencies. A single `npm install` line on your audit.

## Constructing the client

```ts
import { MoonXClient } from '@moon-x/node-sdk';

const moonx = new MoonXClient({
  publishableKey: process.env.MOONX_PUBLIC_API_KEY!,
  // Optional. Pins the issuer against your MoonX deployment. Defaults
  // to https://api.moonx-dev.com if not provided. Set this explicitly
  // for any non-dev environment.
  issuer: process.env.MOONX_AUTH_ISSUER,
});
```

<Info>
  Construct one `MoonXClient` per process and reuse it. The client holds in-memory caches for the publishable-key → app-id resolution (1 hour) and the per-app JWKS (5 minutes). Constructing a fresh client per request defeats both caches.
</Info>

### Configuration

<ParamField path="publishableKey" type="string" required>
  Your `moon_pk_*` publishable key. Used to resolve the app id (which scopes the audience check and JWKS lookup). Safe to read from any env var.
</ParamField>

<ParamField path="secretKey" type="string">
  Optional. `moon_sk_*` secret key: reserved for the v0.2 secret-key surface (data, swaps, server-side wallet ops). The auth-only verification surface does not require it. **Never expose this to the browser.**
</ParamField>

<ParamField path="issuer" type="string" default="https://api.moonx-dev.com">
  The MoonX deployment that signs your tokens. If a token's `iss` doesn't match, verification fails with `IssuerMismatchError`.
</ParamField>

<ParamField path="jwksTtlMs" type="number" default="300000">
  In-memory JWKS cache TTL. Lower if you want faster key-rotation pickup; higher if you want fewer round-trips. Defaults to 5 minutes.
</ParamField>

<ParamField path="appResolveTtlMs" type="number" default="3600000">
  Cache TTL for the publishable-key → app-id mapping. The mapping is effectively immutable, so a long TTL is safe.
</ParamField>

## Verifying a session

`verifySession` is the canonical entry point for "this request is authorized AND we know who the user is." It verifies both tokens and cross-checks that the identity token's subject matches the access token's subject (closing the "swap in someone else's identity token" attack).

```ts
const session = await moonx.auth.verifySession({
  accessToken: req.headers['x-moonx-access-token']!,
  identityToken: req.headers['x-moonx-identity-token'] ?? null,
});

// session.access.sub        - MoonX user id
// session.access.sid        - session id
// session.identity?.email   - OIDC profile (only when identity token was sent)
// session.identity?.name
// session.identity?.picture
```

The verification path runs the full canonical check on each token:

1. **Audience pinning**: rejects tokens issued for other MoonX apps.
2. **Issuer pinning**: rejects tokens from other MoonX deployments.
3. **JWKS fetch**: pulls the per-app public key (cached 5 minutes by default).
4. **ES256 signature verification**: against the fetched JWKS key.
5. **Expiry check**: rejects tokens past their `exp`.
6. **(Identity-token path) Subject cross-check**: rejects if `identity.sub !== access.sub`.

## Individual verify methods

For when you only have one token, or want to verify a token type other than a session pair:

### `verifyAccessToken`

```ts
const access = await moonx.auth.verifyAccessToken(accessToken);
// access.sub, access.sid, access.aud, access.iss, access.exp, access.iat
```

### `verifyIdentityToken`

```ts
const identity = await moonx.auth.verifyIdentityToken(identityToken);
// identity.sub, identity.email?, identity.name?, identity.picture?, etc.
```

### `verifyPresenceToken`

```ts
const presence = await moonx.auth.verifyPresenceToken(presenceToken);
// presence.sub, presence.cid (credential id), presence.exp, presence.tt === "presence"
```

`verifyPresenceToken` additionally enforces `tt === "presence"` so an access or identity token cannot be replayed as a presence proof. See [Presence Tokens](/authentication/user-authentication/presence-tokens) for the full flow.

## Error handling

All verify methods throw a typed `MoonXError` subclass on failure. Catch the subclass to map specific errors to HTTP responses.

```ts
import {
  MoonXError,
  ExpiredTokenError,
  InvalidTokenError,
  AudienceMismatchError,
  IssuerMismatchError,
  SubjectMismatchError,
  KidMismatchError,
  MalformedTokenError,
  UnsupportedAlgorithmError,
  JwksFetchError,
  AppResolutionError,
  ConfigurationError,
} from '@moon-x/node-sdk';

try {
  const session = await moonx.auth.verifySession({ accessToken, identityToken });
  // ...
} catch (e) {
  if (e instanceof ExpiredTokenError) {
    return new Response('Session expired', { status: 401 });
  }
  if (e instanceof MoonXError) {
    // Any other typed MoonX error
    return new Response(e.message, { status: 401 });
  }
  throw e; // Unknown error: bubble up
}
```

### Error class reference

| Class | `.code` | Suggested HTTP | Meaning |
|---|---|---|---|
| `MalformedTokenError` | `malformed_token` | 400 | Not a well-formed JWT |
| `InvalidTokenError` | `invalid_signature` | 401 | Signature didn't verify against the JWKS |
| `ExpiredTokenError` | `expired_token` | 401 | `exp` claim is in the past |
| `AudienceMismatchError` | `audience_mismatch` | 401 | Token's `aud` ≠ your app id |
| `IssuerMismatchError` | `issuer_mismatch` | 401 | Token's `iss` ≠ configured issuer |
| `SubjectMismatchError` | `subject_mismatch` | 401 | Identity-token `sub` ≠ access-token `sub` (suspicious, log it) |
| `KidMismatchError` | `kid_mismatch` | 401 | Likely key rotation; JWKS will re-fetch on retry |
| `UnsupportedAlgorithmError` | `unsupported_algorithm` | 401 | Token uses something other than ES256 |
| `JwksFetchError` | `jwks_fetch_failed` | 503 | Couldn't reach MoonX to refresh JWKS |
| `AppResolutionError` | `app_resolution_failed` | 503 | Publishable-key → app-id lookup failed |
| `ConfigurationError` | `configuration_error` | 500 | Bad `MoonXClient` config; fail loud at boot |

## A reusable `withSession` middleware

A pattern that keeps verification + error mapping in one place. Adapt for your framework (Next.js handler, Express, Hono, etc.).

```ts
import { MoonXClient, MoonXError, ExpiredTokenError } from '@moon-x/node-sdk';
import { NextResponse } from 'next/server';
import type { VerifiedSession, PresenceTokenClaims } from '@moon-x/node-sdk';

const moonx = new MoonXClient({
  publishableKey: process.env.MOONX_PUBLIC_API_KEY!,
  issuer: process.env.MOONX_AUTH_ISSUER,
});

interface Handler<C = unknown> {
  (args: {
    session: VerifiedSession;
    presence?: PresenceTokenClaims;
    request: Request;
    context: C;
  }): Promise<Response> | Response;
}

interface WithSessionOptions {
  requirePresence?: boolean;
}

export function withSession<C = unknown>(
  handler: Handler<C>,
  options: WithSessionOptions = {},
) {
  return async (request: Request, context: C): Promise<Response> => {
    const accessToken = request.headers.get('x-moonx-access-token');
    const identityToken = request.headers.get('x-moonx-identity-token');
    const presenceToken = request.headers.get('x-moonx-presence');

    if (!accessToken) {
      return jsonError(401, 'missing_access_token', 'X-MoonX-Access-Token required');
    }
    if (options.requirePresence && !presenceToken) {
      return jsonError(401, 'missing_presence_token', 'X-MoonX-Presence required');
    }

    try {
      const session = await moonx.auth.verifySession({
        accessToken,
        identityToken: identityToken ?? null,
      });

      let presence: PresenceTokenClaims | undefined;
      if (options.requirePresence && presenceToken) {
        presence = await moonx.auth.verifyPresenceToken(presenceToken);
        if (presence.sub !== session.access.sub) {
          return jsonError(401, 'subject_mismatch', 'Presence subject mismatch');
        }
      }

      return await handler({ session, presence, request, context });
    } catch (e) {
      if (e instanceof ExpiredTokenError) {
        return jsonError(401, e.code, 'Session expired, refresh and retry');
      }
      if (e instanceof MoonXError) {
        return jsonError(401, e.code, e.message);
      }
      console.error('[moonx] unexpected verify error:', e instanceof Error ? e.message : 'unknown error');
      return jsonError(500, 'internal_error', 'Internal server error');
    }
  };
}

function jsonError(status: number, code: string, message: string) {
  return NextResponse.json({ error: { code, message } }, { status });
}
```

Usage in a Next.js App Router route:

```ts
// app/api/me/route.ts
import { withSession } from '@/lib/moonx';

export const GET = withSession(async ({ session }) => {
  return Response.json({
    access: session.access,
    identity: session.identity,
  });
});
```

And a presence-gated route:

```ts
// app/api/transfer/route.ts
export const POST = withSession(
  async ({ session, request }) => {
    const body = await request.json();
    await performTransfer(session.access.sub, body);
    return Response.json({ ok: true });
  },
  { requirePresence: true },
);
```

## Identity-token claim shape

The identity token carries OIDC-standard profile claims. All profile fields are optional. Social-login users who haven't filled out a profile, or email-OTP users who only gave an email, may have only the base claims populated.

```ts
import type { IdentityTokenClaims } from '@moon-x/node-sdk';
// also exported from @moon-x/core/types

const identity: IdentityTokenClaims = await moonx.auth.verifyIdentityToken(token);

// Standard OIDC profile fields (all optional)
identity.email;
identity.name;
identity.given_name;
identity.family_name;
identity.picture;
// plus the BaseTokenClaims fields: sub, aud, iss, iat, exp
```

<Note>
  The MoonX backend may not pass every OIDC claim from the upstream IdP through to the identity token. If your app needs a claim that isn't present, fall back to a direct user-info round-trip or store the value in your own database.
</Note>

## JWKS URL

For non-Node runtimes or custom verification stacks, the per-app JWKS is exposed at:

```
https://api.moonx-dev.com/v0/sdk/.well-known/jwks/<your-app-id>
```

You can fetch this directly and verify ES256 signatures with any JWT library that supports JWKS: `jose`, `panva/jose`, Go's `github.com/lestrrat-go/jwx`, etc. The `@moon-x/node-sdk` is the easiest path on Node-compatible runtimes, but isn't strictly required.

## Related

<CardGroup cols={2}>
  <Card title="Presence Tokens" icon="shield-check" href="/authentication/user-authentication/presence-tokens">
    Gate high-risk backend routes on a fresh passkey assertion.
  </Card>

  <Card title="Sessions Overview" icon="key" href="/authentication/user-authentication/sessions/overview">
    The full session model: what each token type carries and how long it lasts.
  </Card>

  <Card title="Access Tokens" icon="ticket" href="/authentication/user-authentication/access-tokens">
    Access-token claim details and rotation behavior.
  </Card>

  <Card title="Identity Tokens" icon="id-card" href="/authentication/user-authentication/identity-tokens">
    Identity-token claim reference and when to send it.
  </Card>

  <Card title="Token Architecture" icon="circle-question" href="/authentication/user-authentication/sessions/session-token-vs-jwt">
    Why MoonX issues JWKS-verifiable JWTs instead of opaque session tokens.
  </Card>
</CardGroup>
