GitHub - zacharybenson/arken-protocol: Passwordless authentication via DID signatures. No passwords. No sessions. Nothing to breach.

GitHub

5 min read Original article ↗

arken/auth — The server holds nothing.

Passwordless auth middleware for Express and FastAPI. No passwords, no sessions, no user database to breach.

npm version License: MIT Release

Install · How it works · Limits · Join the 1.0 waitlist


Protect a route with one line of middleware

const express = require('express');
const { arkenAuth } = require('arken-auth');

const app = express();

app.get('/protected', arkenAuth(), (req, res) => {
  res.json({ did: req.user.did });
});

An unsigned request gets a 401 back with a one-time nonce. The client signs the nonce with its private key and retries with an Authorization: Signature header. The middleware verifies the signature cryptographically and sets req.user.did — no database lookup, no session, nothing stored.


Install

Note

Requires a Rust toolchain today. The postinstall script compiles the native crypto library. Install Rust from rustup.rs, then re-run the install. Prebuilt binaries ship with 1.0 — no Rust required after that.


Quickstart

Express (Node.js)

Server

// server.js
const express = require('express');
const { arkenAuth } = require('arken-auth');

const app = express();

app.get('/protected', arkenAuth(), (req, res) => {
  res.json({ message: 'Authenticated', did: req.user.did });
});

app.listen(3000);

Options:

arkenAuth({
  statement: 'Sign to access the API.' // shown to the user during signing; optional
})

Client

// client.js
const { ethers } = require('ethers');
const bs58 = require('bs58');

function walletToDidKey(wallet) {
  const signingKey = new ethers.utils.SigningKey(wallet.privateKey);
  const keyBytes = Buffer.from(signingKey.compressedPublicKey.slice(2), 'hex');
  const multicodecKey = Buffer.concat([Buffer.from([0xe7, 0x01]), keyBytes]);
  return `did:key:z${bs58.encode(multicodecKey)}`;
}

async function authenticate(url) {
  const wallet = ethers.Wallet.createRandom();
  const did = walletToDidKey(wallet);

  // 1. Get the challenge
  const r1 = await fetch(url);
  const { challenge } = await r1.json(); // 401
  const { nonce } = challenge;

  // 2. Sign the nonce
  const signature = await wallet.signMessage(ethers.utils.toUtf8Bytes(nonce));

  // 3. Send the signed header
  const r2 = await fetch(url, {
    headers: {
      Authorization: `Signature did="${did}",nonce="${nonce}",signature="${signature}"`
    }
  });

  return r2.json(); // { did: "did:key:zQ3s…" }
}
FastAPI (Python)

Server

# server.py
from fastapi import FastAPI, Depends
from fastapi.responses import JSONResponse
from arken_auth import ArkenAuth

app = FastAPI()

@app.get("/protected")
async def protected(did: str = Depends(ArkenAuth())):
    return {"message": "Authenticated", "did": did}

# Return 401 challenge as JSON (FastAPI raises plain text by default)
@app.exception_handler(401)
async def auth_exception_handler(request, exc):
    if hasattr(exc, "detail") and isinstance(exc.detail, dict):
        return JSONResponse(
            status_code=401,
            content=exc.detail,
            headers=getattr(exc, "headers", {}),
        )
    return JSONResponse(status_code=401, content={"detail": str(exc.detail)})

Options:

ArkenAuth(statement="Sign to access the API.")  # optional

The dependency returns the authenticated did string. On failure it raises HTTPException(401) with { "challenge": { "nonce": "…", "statement": "…" } } as the body.


How it works

Challenge-response flow: GET → 401 with nonce → sign → GET with Authorization header → 200 with DID

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: GET /protected
    S-->>C: 401 { challenge: { nonce, statement } }
    Note over C: sign(nonce, privateKey)
    C->>S: GET /protected<br/>Authorization: Signature did="…",nonce="…",signature="…"
    S-->>C: 200 { did: "did:key:zQ3s…" }
Loading

Challenge. The server generates a random 32-character nonce, stores it in memory with a 60-second TTL, and returns it as JSON on 401. Each nonce is single-use.

Proof. The client signs the nonce with its secp256k1 private key using eth_sign (the Ethereum personal sign scheme). The private key never leaves the client.

Verify and forget. The server uses ECDSA signature recovery (via ethers-rs) to derive the signer's address from the signature alone, then checks it matches the address encoded in the DID. No credentials are stored. The nonce is consumed.


Why Arken Auth

What it means
No passwords Nothing to phish, crack, or reuse across sites
No sessions No cookies, no JWTs, no server-side token store
No user database Nothing to breach; the server forgets after each request
No per-site accounts One key authenticates to any server running Arken Auth
No lock-in The DID format is a W3C open standard

The Arken stone


Identity: did:key

Identity in Arken Auth is a W3C DID Key identifier — a self-describing string derived from a public key:

did:key:zQ3shXgnkuyqVxf3NGBsK1q3J…

The z prefix is the multibase indicator for base58btc encoding. Inside is the multicodec prefix 0xe7 0x01 (secp256k1-pub) followed by the 33-byte compressed public key.

Key type correction. Earlier documentation showed did:key:z6Mk… which is the Ed25519 prefix. This project uses secp256k1 keys (0xe7 0x01), whose correct prefix is zQ3s…. The Rust verifier enforces this.

This identity is portable: it is not bound to a domain, a server, or an account. A user with a did:key can authenticate to any server running Arken Auth without registering anywhere.


Status and limits

  • Not production-ready. This is an early-stage project.
  • Nonces are in memory. They do not survive restarts and will not work across multiple server instances without a shared store.
  • No key recovery. For services, plan for key rotation. Losing the private key means losing access.
  • Rust toolchain required to install. Prebuilt binaries are on the 1.0 roadmap.
  • Aimed at APIs, developer tools, and server-to-server auth — not consumer apps where key management is a burden.

Roadmap to 1.0

  • Prebuilt binaries (no Rust required to install)
  • Shared nonce store (Redis adapter)
  • Key rotation
  • arken-client package (wraps the two-step fetch automatically)
  • did:web and did:pkh support

Security and the Dragon Hunt

See SECURITY.md for the reporting process and scope.

Find a flaw, open a private advisory, and your name goes in the Hall of Finders.


Join the 1.0 waitlist

Founding Builders — everyone who joins before December 15, 2026 — are credited by name in the 1.0 README.

There are 10 design-partner slots for teams who want to shape the 1.0 API directly.

Join the waitlist


Contributing

  • Open an issue before starting significant work — alignment up front saves effort.
  • Run the existing tests before submitting a pull request.
  • Keep the core small: the crypto layer is intentionally minimal.

License

MIT — Arken Protocol