Skip to content

DHEBP/DeroAuth

Repository files navigation

dero-auth

Sign in with your DERO wallet. No email. No password. Just cryptographic proof of wallet ownership.

dero-auth is a TypeScript library that provides wallet-based authentication for DERO — a privacy-focused cryptocurrency with encrypted transactions. It works like "Sign in with Ethereum" (SIWE/EIP-4361), but for DERO's privacy chain.

Privacy advantage: Unlike Ethereum auth, authenticating with DERO doesn't expose your transaction history. Your on-chain activity remains invisible to the service.

Features

  • Pure TypeScript signature verification (Schnorr on BN256) — no wallet needed server-side
  • React components — drop-in <SignInWithDero /> button and useDeroAuth() hook
  • Next.js integration — ready-made API route handlers and auth middleware
  • XSWD client — connects to DERO wallets (Engram, CLI) via WebSocket
  • SIWE-style message format — human-readable, domain-bound, replay-protected
  • JWT sessions — standard token-based session management
  • Zero personal data — just a cryptographic address

Quick Start

Install

npm install dero-auth

Set a JWT Secret (required)

Use a strong secret (at least 32 bytes):

export JWT_SECRET="$(openssl rand -base64 32)"

1. Server: Create API Routes (Next.js App Router)

// app/api/auth/challenge/route.ts
import { createAuthHandlers } from "dero-auth/next";

const { challengeHandler } = createAuthHandlers({
  domain: "myapp.com",
  uri: "https://myapp.com",
  jwtSecret: process.env.JWT_SECRET!,
});

export const POST = challengeHandler;
// app/api/auth/verify/route.ts
import { createAuthHandlers } from "dero-auth/next";

const { verifyHandler } = createAuthHandlers({
  domain: "myapp.com",
  uri: "https://myapp.com",
  jwtSecret: process.env.JWT_SECRET!,
});

export const POST = verifyHandler;

For production deployments, use a shared nonce store (Redis/DB) via the nonceStore option so replay protection works across multiple instances.

Production Nonce Store (Redis)

dero-auth includes a Redis-backed NonceStore adapter with atomic consume:

import { createClient } from "redis";
import { createAuthHandlers } from "dero-auth/next";
import { createRedisNonceStoreFromNodeRedis } from "dero-auth/server";

const redis = createClient({ url: process.env.REDIS_URL! });
await redis.connect();

const nonceStore = createRedisNonceStoreFromNodeRedis(redis, {
  keyPrefix: "myapp:auth:nonce:",
});

const { challengeHandler, verifyHandler } = createAuthHandlers({
  domain: "myapp.com",
  uri: "https://myapp.com",
  jwtSecret: process.env.JWT_SECRET!,
  nonceStore,
});

Install a Redis client of your choice in your app (redis or ioredis).

2. Client: Add the Provider and Button

// app/layout.tsx (or any parent component)
import { DeroAuthProvider } from "dero-auth/react";

export default function Layout({ children }) {
  return (
    <DeroAuthProvider appName="My App">
      {children}
    </DeroAuthProvider>
  );
}
// app/page.tsx (or any page)
import { SignInWithDero } from "dero-auth/react";

export default function LoginPage() {
  return <SignInWithDero />;
}

That's it. The button handles the full flow: wallet connection, challenge signing, and verification.

3. Custom Hook (Alternative)

If you don't want the provider pattern:

"use client";
import { useDeroAuth } from "dero-auth/react";

export default function LoginPage() {
  const { signIn, signOut, isAuthenticated, address, isLoading } = useDeroAuth();

  if (isAuthenticated) {
    return (
      <div>
        <p>Signed in as {address}</p>
        <button onClick={signOut}>Sign Out</button>
      </div>
    );
  }

  return (
    <button onClick={signIn} disabled={isLoading}>
      {isLoading ? "Connecting..." : "Sign In with DERO"}
    </button>
  );
}

How It Works

Authentication Flow

User                    Browser                   Server
 |                         |                         |
 | Click "Sign In"         |                         |
 |------------------------>|                         |
 |                         | POST /auth/challenge     |
 |                         |   { address }            |
 |                         |------------------------>|
 |                         |   { messageText, nonce } |
 |                         |<------------------------|
 |                         |                         |
 |  Wallet: "Sign this?"   | XSWD: SignData(message) |
 |  [Approve] [Deny]       |                         |
 |------------------------>|                         |
 |  PEM signature           |                         |
 |                         | POST /auth/verify        |
 |                         |   { signature, nonce }   |
 |                         |------------------------>|
 |                         |     Verify Schnorr sig   |
 |                         |     Check domain/nonce   |
 |                         |   { session (JWT) }      |
 |                         |<------------------------|
 |  Authenticated!          |                         |

Message Format

The message shown to the user in their wallet:

myapp.com wants you to sign in with your DERO wallet:
dero1qy...abc123

Sign in to My App

URI: https://myapp.com
Version: 1
Chain ID: dero-mainnet
Nonce: k8j2m4n6p8q0
Issued At: 2026-02-14T12:00:00.000Z
Expiration Time: 2026-02-14T12:05:00.000Z

Domain Binding

Every signature is bound to a domain — an identifier you pass to the server that must match the one baked into the signed message. This is what stops a signature made for one app from being replayed against another. It's a plain string (not a DNS lookup), so it works anywhere:

  • Web app → your host, e.g. "myapp.com"
  • TELA app (DERO on-chain web app) → the app's INDEX SCID (its on-chain identity)
  • ENS / IPFS dApp → the ENS name or CID

The domain is required: both createAuthHandlers({ domain }) and the low-level verifyAuth(sig, { domain }) fail closed if it's missing, so an unbound (replayable) signature is never accepted.

Cryptography

  • Signature scheme: Schnorr signatures on BN256 (Barreto-Naehrig 254-bit curve)
  • Hash function: Keccak-256, reduced modulo the curve order
  • Address format: Bech32 with 33-byte compressed G1 public key
  • Verification: Pure TypeScript using @noble/curves (audited by Cure53)
  • No blockchain query needed: Signatures are verified mathematically, offline

Package Exports

Import Description
dero-auth Core types and message format
dero-auth/crypto Pure crypto: Schnorr verify, BN256, address parsing
dero-auth/server Server SDK: challenges, verification, JWT sessions
dero-auth/client Browser SDK: XSWD wallet connection, signing flow
dero-auth/react React: Provider, button, hooks
dero-auth/next Next.js: API handlers, auth middleware

API Reference

Core (dero-auth)

import {
  serializeMessage,   // DeroAuthMessage → string
  parseMessage,        // string → DeroAuthMessage
  validateMessage,     // Check domain, nonce, expiry
  generateNonce,       // Crypto-random nonce string
  type DeroAuthMessage,
  type DeroAuthConfig,
  type DeroAuthSession,
} from "dero-auth";

Crypto (dero-auth/crypto)

import {
  verifySignature,     // Verify PEM signature → { address, message }
  parsePEM,            // Parse DERO PEM format
  decodeAddress,       // DERO address → public key
  encodeAddress,       // Public key → DERO address
  isValidAddress,      // Validate address format
  reducedHash,         // Keccak256 mod curve order
  decompressG1,        // 33 bytes → G1 point
  compressG1,          // G1 point → 33 bytes
} from "dero-auth/crypto";

Server (dero-auth/server)

import {
  createChallengeServer,  // Challenge generation + nonce management
  verifyAuth,             // Full verification pipeline
  createSessionManager,   // JWT session creation/verification
} from "dero-auth/server";

Client (dero-auth/client)

import {
  XSWDClient,         // Low-level XSWD WebSocket client
  createAuthClient,   // High-level auth flow client
} from "dero-auth/client";

React (dero-auth/react)

import {
  DeroAuthProvider,    // Context provider
  SignInWithDero,      // Drop-in button
  useDeroAuth,         // Standalone hook
  useDeroAuthContext,  // Hook for use within provider
} from "dero-auth/react";

Next.js (dero-auth/next)

import {
  createAuthHandlers,        // API route handlers
  createDeroAuthMiddleware,  // Auth middleware
  getAuthAddress,            // Extract address from request
} from "dero-auth/next";

Protecting Routes (Next.js Middleware)

// middleware.ts
import { createDeroAuthMiddleware } from "dero-auth/next";

export const middleware = createDeroAuthMiddleware({
  jwtSecret: process.env.JWT_SECRET!,
  protectedPaths: ["/dashboard", "/api/protected"],
  loginPath: "/login",
});

export const config = {
  matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};

Requirements

  • DERO wallet: Engram (desktop) or DERO CLI wallet with XSWD enabled
  • Node.js: >= 20.19.0
  • React: 18+ or 19+ (optional, for React components)
  • Next.js: 14+ (optional, for Next.js helpers)

Wallet Compatibility

Wallet Connection Status
Engram (desktop) XSWD (ws://localhost:44326) Supported
DERO CLI wallet XSWD (ws://localhost:10103) Supported

Why Not Just Use Ethereum Auth?

Ethereum (SIWE) DERO Auth
Transaction privacy All transactions visible on-chain Transactions encrypted, invisible to verifier
Wallet extension MetaMask required Connects to local wallet via WebSocket
Personal data Address links to full history Address reveals nothing about activity
Verification Pure JS (ecrecover) Pure JS (Schnorr on BN256)

Development

# Install dependencies
npm install

# Run tests
npm test

# Type check
npx tsc --noEmit

# Build
npm run build

# Run demo app
JWT_SECRET="$(openssl rand -base64 32)" cd demo && npm install && npm run dev

The XSWD client is silent by default. For verbose protocol logs while debugging locally (Node only — never in the browser), set DERO_AUTH_DEBUG=1.

License

MIT — see LICENSE

DHEBP is not affiliated with or endorsed by the DERO Project or its core developers.

About

Login to websites and apps with your DERO wallet

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors