NERO
Guides

Use Your Own Auth System

Authenticate users with your existing identity provider via custom JWT

Use Your Own Auth System

Bring your existing authentication system (Auth0, Firebase, Cognito, or any OIDC provider) and use its JWTs to create MPC wallets. Users authenticate through your identity provider as usual, and the NERO SDK verifies the JWT to provision a wallet tied to that identity.

Prerequisites

  • @nerochain/mpc-sdk installed
  • An OIDC-compliant identity provider that issues JWTs (Auth0, Firebase Auth, AWS Cognito, Okta, etc.)
  • Your provider's issuer URL and JWKS endpoint

How It Works

  1. Register a verifier -- tell the NERO backend how to validate JWTs from your provider
  2. User authenticates -- through your existing login flow (your UI, your provider)
  3. Pass the JWT -- call loginWithCustomJwt() with the user's ID token and your verifier ID
  4. Wallet is created -- the SDK verifies the JWT signature against your provider's JWKS, authenticates the user, and generates an MPC wallet tied to that identity

Step 1 -- Register a JWT Verifier

Before users can log in, register your identity provider as a verifier. Do this once per provider, either through the NERO dashboard or programmatically via useNeroAdmin.

Option A: Via the Dashboard

  1. Go to your project settings in the NERO dashboard
  2. Navigate to "JWT Verifiers"
  3. Click "Add Verifier"
  4. Fill in your provider details:
    • Name: A label for this verifier (e.g., "Production Auth0")
    • Type: oidc or firebase
    • Issuer URL: Your provider's issuer (e.g., https://my-tenant.auth0.com/)
    • JWKS URL: The public key endpoint (e.g., https://my-tenant.auth0.com/.well-known/jwks.json)
    • Audience: The audience claim expected in the JWT (your API identifier)
  5. Save and copy the Verifier ID

Option B: Programmatically

Use the useNeroAdmin hook to create a verifier from your application code. This requires an authenticated admin session.

import { useNeroAdmin } from "@nerochain/mpc-sdk/react";

function VerifierSetup() {
  const { createVerifier, listVerifiers, isLoading, error } = useNeroAdmin();
  const [verifierId, setVerifierId] = useState<string | null>(null);

  const handleCreate = async () => {
    const result = await createVerifier({
      name: "My Auth0",
      type: "oidc",
      issuer: "https://my-tenant.auth0.com/",
      jwksUrl: "https://my-tenant.auth0.com/.well-known/jwks.json",
      audience: "my-api",
    });
    setVerifierId(result.id);
  };

  const handleList = async () => {
    const verifiers = await listVerifiers();
    console.log("Registered verifiers:", verifiers);
  };

  return (
    <div>
      <button onClick={handleCreate} disabled={isLoading}>
        Create Verifier
      </button>
      <button onClick={handleList} disabled={isLoading}>
        List Verifiers
      </button>
      {verifierId && <p>Verifier ID: <code>{verifierId}</code></p>}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

The createVerifier config accepts:

FieldTypeRequiredDescription
namestringYesDisplay name for this verifier
type"oidc" | "firebase"YesProvider type
issuerstringYesThe iss claim in the JWT (must match exactly)
jwksUrlstringNoURL to fetch public keys for signature verification. Defaults to {issuer}/.well-known/jwks.json
audiencestringNoExpected aud claim in the JWT

Step 2 -- Authenticate Users with Your Provider

Your existing auth flow stays unchanged. Users log in through your UI, and you receive a JWT (ID token) from your provider. The specific implementation depends on your provider:

// Auth0 example
const { getIdTokenClaims } = useAuth0();
const claims = await getIdTokenClaims();
const idToken = claims.__raw;

// Firebase example
const idToken = await firebase.auth().currentUser.getIdToken();

// Generic OIDC
const idToken = sessionStorage.getItem("id_token");

Step 3 -- Log In with the JWT

Pass the JWT to loginWithCustomJwt() along with the verifier ID from step 1. The SDK validates the token against your provider's JWKS and creates (or restores) an MPC wallet for that user identity.

import { useNeroConnect } from "@nerochain/mpc-sdk/react";

function CustomLogin() {
  const { loginWithCustomJwt, isConnecting, error } = useNeroConnect();

  const handleLogin = async (idToken: string) => {
    await loginWithCustomJwt({
      idToken,
      verifierId: "your-verifier-id",
    });
    // User is now authenticated with an MPC wallet
  };

  return (
    <div>
      <button onClick={() => handleLogin(getTokenFromYourAuth())} disabled={isConnecting}>
        {isConnecting ? "Creating wallet..." : "Connect Wallet"}
      </button>
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

Complete Working Component

This example shows the full flow: managing verifiers (admin side) and logging in with a JWT (user side).

import { useState } from "react";
import { useNeroAdmin, useNeroConnect, useNeroWallet } from "@nerochain/mpc-sdk/react";

export default function CustomAuthGuide() {
  const { createVerifier, listVerifiers, isLoading: adminLoading, error: adminError } = useNeroAdmin();
  const { loginWithCustomJwt, isConnecting, error: connectError } = useNeroConnect();
  const { wallet } = useNeroWallet();

  const [tab, setTab] = useState<"setup" | "login">("setup");
  const [verifierId, setVerifierId] = useState("");
  const [jwt, setJwt] = useState("");
  const [verifiers, setVerifiers] = useState<unknown[]>([]);

  // Verifier config fields
  const [name, setName] = useState("My Auth0");
  const [type, setType] = useState<"oidc" | "firebase">("oidc");
  const [issuer, setIssuer] = useState("");
  const [jwksUrl, setJwksUrl] = useState("");
  const [audience, setAudience] = useState("");

  const handleCreateVerifier = async () => {
    try {
      const config: Parameters<typeof createVerifier>[0] = { name, type, issuer };
      if (jwksUrl) config.jwksUrl = jwksUrl;
      if (audience) config.audience = audience;
      const result = await createVerifier(config);
      setVerifierId(result.id);
    } catch {
      // error state handled by hook
    }
  };

  const handleListVerifiers = async () => {
    try {
      const result = await listVerifiers();
      setVerifiers(result as unknown[]);
    } catch {
      // error state handled by hook
    }
  };

  const handleLogin = async () => {
    if (!jwt || !verifierId) return;
    try {
      await loginWithCustomJwt({ idToken: jwt, verifierId });
    } catch {
      // error state handled by hook
    }
  };

  const error = adminError ?? connectError;

  return (
    <div style={{ maxWidth: 520, margin: "0 auto" }}>
      <h2>Custom JWT Authentication</h2>

      <div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
        <button
          onClick={() => setTab("setup")}
          style={{ fontWeight: tab === "setup" ? "bold" : "normal" }}
        >
          1. Setup Verifier
        </button>
        <button
          onClick={() => setTab("login")}
          style={{ fontWeight: tab === "login" ? "bold" : "normal" }}
        >
          2. Login with JWT
        </button>
      </div>

      {error && <p style={{ color: "red" }}>{error.message}</p>}

      {tab === "setup" && (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Verifier name" />
          <select value={type} onChange={(e) => setType(e.target.value as "oidc")}>
            <option value="oidc">OIDC</option>
            <option value="firebase">Firebase</option>
          </select>
          <input value={issuer} onChange={(e) => setIssuer(e.target.value)} placeholder="Issuer URL (required)" />
          <input value={jwksUrl} onChange={(e) => setJwksUrl(e.target.value)} placeholder="JWKS URL (optional)" />
          <input value={audience} onChange={(e) => setAudience(e.target.value)} placeholder="Audience (optional)" />

          <div style={{ display: "flex", gap: 8 }}>
            <button onClick={handleCreateVerifier} disabled={adminLoading || !issuer}>
              Create Verifier
            </button>
            <button onClick={handleListVerifiers} disabled={adminLoading}>
              List Verifiers
            </button>
          </div>

          {verifierId && <p>Verifier ID: <code>{verifierId}</code></p>}
          {verifiers.length > 0 && <pre>{JSON.stringify(verifiers, null, 2)}</pre>}
        </div>
      )}

      {tab === "login" && (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          <input
            value={verifierId}
            onChange={(e) => setVerifierId(e.target.value)}
            placeholder="Verifier ID"
          />
          <textarea
            value={jwt}
            onChange={(e) => setJwt(e.target.value)}
            placeholder="Paste your JWT (ID token) here"
            rows={4}
            style={{ fontFamily: "monospace", fontSize: 12 }}
          />
          <button onClick={handleLogin} disabled={isConnecting || !jwt || !verifierId}>
            {isConnecting ? "Authenticating..." : "Login with JWT"}
          </button>

          {wallet && (
            <div style={{ padding: 12, background: "#f0fdf4", borderRadius: 8, marginTop: 8 }}>
              <p>Wallet created: <code>{wallet.eoaAddress}</code></p>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

What the User Sees

  1. Your login page -- the user logs in through your existing auth UI (unchanged)
  2. Wallet provisioning -- a brief "Creating wallet..." state while the SDK verifies the JWT and runs DKG
  3. Connected -- the user has an MPC wallet tied to their existing identity, with no separate "crypto login" step

Provider-Specific Configuration

Auth0

await createVerifier({
  name: "Auth0 Production",
  type: "oidc",
  issuer: "https://YOUR_DOMAIN.auth0.com/",
  audience: "YOUR_API_IDENTIFIER",
});

Firebase Auth

await createVerifier({
  name: "Firebase Production",
  type: "firebase",
  issuer: "https://securetoken.google.com/YOUR_PROJECT_ID",
});

AWS Cognito

await createVerifier({
  name: "Cognito Production",
  type: "oidc",
  issuer: "https://cognito-idp.REGION.amazonaws.com/USER_POOL_ID",
  jwksUrl: "https://cognito-idp.REGION.amazonaws.com/USER_POOL_ID/.well-known/jwks.json",
});

Error Handling

try {
  await loginWithCustomJwt({ idToken, verifierId });
} catch (err) {
  if (err.message.includes("invalid_token")) {
    // JWT signature verification failed
  } else if (err.message.includes("expired")) {
    // JWT has expired -- refresh it from your provider
  } else if (err.message.includes("verifier_not_found")) {
    // Verifier ID doesn't exist -- check the dashboard
  }
}
ErrorCauseFix
Invalid tokenJWT signature doesn't match the JWKS public keysEnsure the issuer URL and JWKS endpoint are correct
Token expiredThe JWT exp claim is in the pastGet a fresh token from your identity provider
Verifier not foundThe verifierId doesn't match a registered verifierCheck the verifier ID in the dashboard or call listVerifiers()
Audience mismatchJWT aud claim doesn't match the verifier's audience configUpdate the audience in the verifier config or in your provider settings
Issuer mismatchJWT iss claim doesn't match the verifier's issuer URLEnsure the issuer URL includes the trailing slash if your provider uses one

On this page