Arkena Docs

Sign-In with Canton

SIWC is Arkena's authentication primitive. The shape mirrors EIP-4361 with Canton-native fields.

Sign-In with Canton (SIWC) is the equivalent of Sign-In with Ethereum. The backend issues a structured message with a nonce, the user signs it with their wallet, and the backend issues a JWT scoped to the user's party. You attach the JWT on subsequent calls.

SIWC is not a transaction signature. It does not authorise on-chain state changes — those still go through prepare/execute with a separate signature.

The flow

Code
// 1. Backend issues a challenge
const { message, nonce } = await fetch("/api/auth/nonce", {
  method: "POST",
  body: JSON.stringify({ partyId: party }),
  headers: { "Content-Type": "application/json" },
}).then((r) => r.json());

// 2. Wallet signs the message
const { signature } = await provider.request({
  method: "canton_signMessage",
  params: { message },
});

// 3. Backend verifies and returns a JWT
const { token, expiresAt } = await fetch("/api/auth/verify", {
  method: "POST",
  body: JSON.stringify({ partyId: party, signature, nonce }),
  headers: { "Content-Type": "application/json" },
}).then((r) => r.json());

// 4. Use the token
await fetch("/api/wallet/balance/" + party, {
  headers: { Authorization: `Bearer ${token}` },
});

The structured message

The challenge message follows EIP-4361 conventions, adapted for Canton. Example:

Code
arkena.io wants you to sign in with your Canton party:
alice::1220abc...

URI: https://arkena.io
Version: 1
Network: mainnet
Nonce: 9b2d0c4f81a3...
Issued At: 2026-05-04T16:00:00Z
Expiration Time: 2026-05-04T16:10:00Z
Statement: I accept the Arkena Terms of Service.

Fields:

  • Domain (first line) — the host the user is signing in to. Lets the user verify they're signing for the right site.
  • Party — the party ID this signature claims to be from.
  • URI, Version, Network — boilerplate.
  • Nonce — single-use, server-generated. Prevents replay.
  • Issued At, Expiration Time — the challenge message lifetime (10 minutes from issue). After expiry the user has to request a fresh challenge before signing.
  • Statement — optional human-readable claim shown in the wallet prompt.

The wallet renders this as a sign-in card with the domain and party prominent. The user clicks once.

The JWT

The backend's response carries:

  • token — the JWT to attach as Authorization: Bearer <token>.
  • expiresAt — Unix timestamp.

The JWT lives for 4 hours from issue. The backend silently rotates it on any authenticated call older than 30 minutes (handing back a fresh Set-Cookie so the cookie copy stays current), and accepts the previous token for a 5-minute grace window after rotation. You almost never need to manage the lifecycle manually — but if you want to, the manual flow below works.

The token is scoped to the single partyId it was issued for. Trying to use it for another party returns 403 Forbidden. To inspect the party attached to a token at any time, call GET /api/auth/me.

Rotation

If you're holding the JWT yourself (e.g. native client, no cookie), request a fresh challenge and re-sign before expiresAt. Rotation is silent — the wallet doesn't show a prompt for already-trusted origins on the same party.

To explicitly end a session, call POST /api/auth/logout.

What SIWC is not

  • Not a transaction signature. SIWC signatures don't authorise on-ledger state changes.
  • Not a permission grant. Connecting (canton_connect) and signing in are separate. A user can be connected without ever signing in; signing in implies connection.
  • Not persistent across browsers. Each browser instance has its own JWT lifecycle.

What's next