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

# JWT User Authentication

> Authenticate your users to Etherfuse with short-lived JWTs you sign, and launch them straight into the app.

Your users authenticate to Etherfuse with JSON Web Tokens that you sign. You sign a short-lived JWT for a user, and we verify it against a JWKS URL you've registered with us. It's an ordinary OAuth 2.0 JWT bearer exchange ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)), so any JWT library works.

The simplest and recommended way to use a JWT is to **launch the user straight into the Etherfuse app**, where they complete flows like KYB. You can also [exchange a JWT server-side](/api-reference/authentication/exchange-a-partner-jwt) to provision a user ahead of time or prefetch a refresh token that speeds up launch.

## Before you start

<Steps>
  <Step title="Generate a signing key pair">
    Create an RSA key pair for `RS256`, the signing algorithm we verify. You sign JWTs with the **private** key and publish the **public** key (next step).

    ```bash theme={null}
    # RSA 2048
    openssl genrsa -out private.pem 2048
    openssl rsa -in private.pem -pubout -out public.pem
    ```
  </Step>

  <Step title="Host a JWKS endpoint">
    Publish the **public** key as a JSON Web Key Set ([RFC 7517](https://www.rfc-editor.org/rfc/rfc7517)) at a stable HTTPS URL, e.g. `https://your-domain.com/.well-known/jwks.json`. Give the key a stable id (`kid`) so we can match it to a token's `kid` header. Most JWT libraries can export a public key to JWK form:

    <CodeGroup>
      ```json Key set theme={null}
      {
        "keys": [
          {
            "kty": "RSA",
            "use": "sig",
            "alg": "RS256",
            "kid": "your-key-id",
            "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
            "e": "AQAB"
          }
        ]
      }
      ```

      ```javascript Node (jose) theme={null}
      import express from "express";
      import { exportJWK } from "jose";
      import { createPublicKey } from "crypto";

      const jwk = await exportJWK(createPublicKey(publicPem));
      const jwks = { keys: [{ ...jwk, kid: "your-key-id", use: "sig", alg: "RS256" }] };

      express()
        .get("/.well-known/jwks.json", (_req, res) => res.json(jwks))
        .listen(3000);
      ```

      ```python Python (PyJWT) theme={null}
      import json
      from flask import Flask, jsonify
      from jwt.algorithms import RSAAlgorithm

      jwk = json.loads(RSAAlgorithm.to_jwk(public_key))
      jwk.update({"kid": "your-key-id", "use": "sig", "alg": "RS256"})
      jwks = {"keys": [jwk]}

      app = Flask(__name__)

      @app.get("/.well-known/jwks.json")
      def jwks_endpoint():
          return jsonify(jwks)
      ```
    </CodeGroup>

    The URL must be reachable from Etherfuse's servers over HTTPS and return the key set as JSON. We fetch it fresh on every verification (no caching), so **rotating keys** is straightforward: add the new key as another entry in `keys`, start signing with its `kid`, then remove the old key once no unexpired tokens still reference it.
  </Step>

  <Step title="Register your issuer with Etherfuse">
    Send your Etherfuse representative your **issuer** (`iss`) and **JWKS URL**. We link them to your partner organization so JWTs from that issuer resolve to your customers. Register separately for **sandbox** and **production**: each environment keeps its own issuer registration and uses its own `aud` (the sandbox vs production token endpoint). (There is no self-serve endpoint for this *yet*.)
  </Step>
</Steps>

## Sign a user JWT

Sign a JWT with the key behind your JWKS. The claims:

| Claim           | Required | Notes                                                                                                                                                       |
| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss`           | ✓        | Your registered issuer.                                                                                                                                     |
| `sub`           | ✓        | The user's id. A UUID is *strongly recommended*; see the note below.                                                                                        |
| `aud`           | ✓        | The token endpoint URL: `https://api.etherfuse.com/auth/token` (or `https://api.sand.etherfuse.com/auth/token` in sandbox).                                 |
| `scope`         | ✓        | Required. Names the flow the user may complete. See [Scopes](/guides/user-launch-flows#scopes).                                                             |
| `jti` / `nonce` | ✓        | A fresh value, unique per token, for replay protection. You can send it as `jti` (the standard JWT ID) or `nonce` (the standard OIDC claim).                |
| `email`, `name` | ✓        | The user's email and full name.                                                                                                                             |
| `exp`, `iat`    | ✓        | Keep tokens short-lived (about 5 minutes). There is no enforced maximum lifetime, but verification allows no clock skew, so keep your server clock in sync. |
| `picture`       | optional | Profile image URL.                                                                                                                                          |

<Note>
  **Strongly recommended: make `sub` a UUID.** `sub` identifies the person signing in. For an individual customer, that person is the customer: use the same UUID for `sub` and for the `customerId` in the Ramp API, so both resolve to one customer. A non-UUID `sub` still signs in, but can't be addressed through the Ramp API.
</Note>

<Warning>
  **A `sub` must name a person, never a business.** Don't put a business organization's id in `sub`: it's rejected with `invalid_grant`. And don't reuse a person's `sub` as a business org id: [`POST /ramp/organization`](/api-reference/organizations/create-child-org) returns `409`. A business is a separate org with its own id; a real person signs in with their own personal `sub` and completes the business's verification on its behalf.
</Warning>

<CodeGroup>
  ```javascript Node theme={null}
  import jwt from "jsonwebtoken";
  import { randomUUID } from "crypto";

  // Reuse one UUID per user; it's their customerId in the Ramp API too.
  const customerId = randomUUID();

  const token = jwt.sign(
    {
      scope: "kyb",
      jti: randomUUID(),
      email: "ana@example.com",
      name: "Ana García",
    },
    PRIVATE_KEY,                       // the key behind your JWKS
    {
      algorithm: "RS256",
      keyid: "your-key-id",            // matches a kid in your JWKS
      issuer: "https://your-domain.com",
      subject: customerId,
      audience: "https://api.sand.etherfuse.com/auth/token",
      expiresIn: "5m",
    },
  );
  ```

  ```python Python theme={null}
  import uuid, time, jwt   # PyJWT

  customer_id = str(uuid.uuid4())   # also the customerId in the Ramp API

  token = jwt.encode(
      {
          "iss": "https://your-domain.com",
          "sub": customer_id,
          "aud": "https://api.sand.etherfuse.com/auth/token",
          "scope": "kyb",
          "jti": str(uuid.uuid4()),
          "email": "ana@example.com",
          "name": "Ana García",
          "iat": int(time.time()),
          "exp": int(time.time()) + 300,
      },
      PRIVATE_KEY,                    # the key behind your JWKS
      algorithm="RS256",
      headers={"kid": "your-key-id"},
  )
  ```
</CodeGroup>

## Launch the user into the app

This is the recommended path. You hand the user's browser to Etherfuse with their JWT, and the app signs them in and takes them where you point them (for example, [KYB](/guides/kyb)).

<Warning>
  **Launch lives on the app host**, not the API host: `https://app.etherfuse.com` (`https://sandbox.etherfuse.com` in sandbox). It's a browser page, not a JSON API.
</Warning>

Every launch carries:

* **`grant_type`** and **`assertion`**: either a raw partner JWT (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`) or a `refresh_token` from a prior [server-side exchange](/api-reference/authentication/exchange-a-partner-jwt) (`grant_type=refresh_token`).
* **`target`**: the app path to land on; see [Targets](/guides/user-launch-flows#targets) for the available paths.
* **`return_url`** *(optional)*: where to send the user when they leave or the session can't resume. It is not the success redirect; on success the user lands on `target`.

Have the user's browser POST a form to `/auth/launch`, either as a top-level navigation or into an iframe you've embedded (point the form's `target` at the frame). The app exchanges the JWT, establishes the session, and redirects to `target`. A server-rendered auto-submitting form is the usual way:

```html theme={null}
<form id="launch" method="POST" action="https://sandbox.etherfuse.com/auth/launch">
  <input type="hidden" name="grant_type" value="urn:ietf:params:oauth:grant-type:jwt-bearer" />
  <input type="hidden" name="assertion" value="<your_signed_jwt>" />
  <input type="hidden" name="target" value="/kyb" />
</form>
<script>document.getElementById("launch").submit()</script>
```

<Note>
  **Rather not POST a form?** Hand the credentials to the launch page over `postMessage` instead: open it in an iframe or popup and message the JWT in. See [Launch via postMessage](/api-reference/launch-via-postmessage) for the message contract and a working example.
</Note>

<Note>
  **Embedding.** You can embed the launch page in an iframe or popup on your own site; there's no origin to register with us. With `postMessage`, trust is established by the handshake itself: the page only accepts a reply from the parent or opener that requested it. Flows that use the camera, such as [`/idv`](/guides/user-launch-flows#identity-verification), additionally need `allow="camera *; microphone *"` on the iframe element (the wildcard origin is required because the scan SDK uses the camera from a nested frame; a bare `allow="camera"` only covers the iframe's own origin).
</Note>

## Scopes and targets

Which [`scope`](/guides/user-launch-flows#scopes) to sign and which [`target`](/guides/user-launch-flows#targets) to launch into depends on the flow you're sending the user into. Every available flow (and what to know about each) is listed in **[User Launch Flows](/guides/user-launch-flows#flows)**.

## When something fails

Auth failures come back as OAuth errors with an `error`, an `error_description`, and an `error_uri` pointing at the matching entry in [Authentication errors](/guides/authentication-errors).

A launch renders these on the page rather than returning them. If you want to read them programmatically, [exchange the JWT server-side](/api-reference/authentication/exchange-a-partner-jwt) first: `POST /auth/token` returns the same error as JSON, so you can branch on `error` before ever handing the user to a launch.
