---
title: "3. ROLA Authentication"
url: "https://radix-wiki.miow.me/developers/frontend/03-rola-authentication"
updated: 2026-10-03
description: "ROLA (Radix Off-Ledger Authentication) is a challenge-response protocol that lets your backend verify a user owns a Radix account or persona – without…"
---

# 3. ROLA Authentication

|  |  |
| --- | --- |
| NPM | [@radixdlt/rola](https://www.npmjs.com/package/@radixdlt/rola) |

# 3. ROLA Authentication

## What is [ROLA](https://docs.radixdlt.com/docs/rola-radix-off-ledger-auth)?

[ROLA](https://docs.radixdlt.com/docs/rola-radix-off-ledger-auth) (Radix Off-Ledger Authentication) is a challenge-response protocol that lets your backend verify a user owns a Radix account or [persona](/contents/tech/core-protocols/personas) – without submitting any on-ledger transaction. It's the Radix equivalent of "Sign-In with [Ethereum](https://ethereum.org)" but uses the native [Radix Wallet](/contents/tech/core-protocols/radix-wallet).

## How It Works

1. **Backend generates a challenge** – a random 32-byte hex string stored with a 5-minute expiry
2. **Frontend requests proof** – asks the [Radix Wallet](/contents/tech/core-protocols/radix-wallet) for accounts or a persona _with proof_, and the toolkit attaches the challenge it gets from the generator you registered
3. **User approves in wallet** – the wallet does not sign the bare challenge. It signs a [blake2b hash](https://github.com/radixdlt/rola/blob/main/typescript/src/helpers/create-signature-message.ts) of the byte `R`, the challenge, the length and text of your dApp definition address, and your website's origin, using the key behind each account or persona ([Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) or [secp256k1](https://en.bitcoin.it/wiki/Secp256k1)). Binding the origin and dApp definition is what stops a proof captured on one site from logging anyone into another
4. **Frontend sends proof to backend** – the address, its type (`account` or `persona`), the challenge, and the public key, signature and curve
5. **Backend verifies** – `verifySignedChallenge` [rebuilds the same hash](https://github.com/radixdlt/rola/blob/main/typescript/src/rola.ts) from the `expectedOrigin` and `dAppDefinitionAddress` you configured, checks the signature, then asks the Gateway for the address's `owner_keys` metadata. That metadata holds _hashes_ of public keys, so the check is that the hash of the presented key is among them. If the address has no `owner_keys` yet, which is the case for an account or persona that has never been touched on ledger, it passes only when the address is the one [derived from the public key](https://github.com/radixdlt/rola/blob/main/typescript/src/helpers/derive-address-from-public-key.ts) itself
6. **Your code deletes the challenge** – the package checks neither expiry nor reuse, and it never sees your challenge store. Look the challenge up, reject it if expired, and delete it before or as you call `verifySignedChallenge`

## Implementation

### 1. Generate Challenges (Backend)

```
import crypto from 'crypto'

// Store challenges with expiry (use Redis, DB, or in-memory Map)
const challenges = new Map()

function createChallenge(): string {
  const challenge = crypto.randomBytes(32).toString('hex')
  challenges.set(challenge, {
    expires: Date.now() + 5 * 60 * 1000  // 5 minutes
  })
  return challenge
}
```

### 2. Request Proof (Frontend)

The challenge is never passed to the request builder. You give the [Radix dApp Toolkit](/developers/frontend/01-radix-dapp-toolkit) a generator function, and the toolkit calls that function itself each time it sends a request. `withProof()` is a flag on the builder, not a place to put the challenge.

```
import { DataRequestBuilder } from '@radixdlt/radix-dapp-toolkit'

// RDT calls this once per request and attaches the result to it
rdt.walletApi.provideChallengeGenerator(async () => {
  const res = await fetch('/api/auth/challenge')
  return (await res.json()).challenge
})

// withProof() takes an optional boolean, never the challenge string
rdt.walletApi.setRequestData(
  DataRequestBuilder.accounts().atLeast(1).withProof(),
)

const result = await rdt.walletApi.sendRequest()   // takes no arguments

if (result.isOk()) {
  const { proofs } = result.value   // one signed challenge per shared account or persona
}
```

Two things here are easy to get wrong. `walletApi.sendRequest()` accepts no arguments: the builders go to `setRequestData()` first, or to `sendOneTimeRequest(...)` if you do not want the request stored for later connections. And every entry in `result.value.proofs` already has the shape `{ address, type, challenge, proof }`, which is exactly the argument `verifySignedChallenge` expects in the next step, so you can pass one straight through without rebuilding it. The `curve` field is the string `'curve25519'` or `'secp256k1'`, so do not test it against `'ed25519'` even though Ed25519 is the signature scheme.

### Proving a persona instead

For a login, a persona is often the better thing to prove: it is the identity the user chose to show your dApp, and it stays the same when they share a different set of accounts next time. The builder is `DataRequestBuilder.persona().withProof()`, and the entry it adds to `proofs` carries `type: 'persona'` and the persona's `identity_rdx1…` address, which `verifySignedChallenge` handles with no other change.

```
rdt.walletApi.setRequestData(
  DataRequestBuilder.persona().withProof(),
  DataRequestBuilder.accounts().atLeast(1).withProof(),
)
```

To re-prove addresses the user has already shared, without asking them to pick again, the one-time builder has `OneTimeDataRequestBuilder.proofOfOwnership().identity(identityAddress).accounts([accountAddress])`, sent with `sendOneTimeRequest(...)`.

### 3. Verify Proof (Backend)

```
import { Rola } from '@radixdlt/rola'
import { RadixNetwork } from '@radixdlt/babylon-gateway-api-sdk'

const rola = Rola({
  networkId: RadixNetwork.Mainnet,
  applicationName: 'My dApp',
  dAppDefinitionAddress: 'account_rdx...',
  expectedOrigin: 'https://my-dapp.com'
})

// Verify the signed challenge
const result = await rola.verifySignedChallenge({
  challenge: challengeHex,
  proof: { publicKey, signature, curve },
  address: accountAddress,
  type: 'account'
})

if (result.isOk()) {
  // Create session (JWT, cookie, etc.)
}
```

Delete used challenges

Always delete a challenge after verification – successful or not. `verifySignedChallenge` does not do it for you: it has no access to your store, so without this step a captured proof can be resubmitted for as long as the challenge exists.

## When to Use [ROLA](https://docs.radixdlt.com/docs/rola-radix-off-ledger-auth)

| Use Case | Auth Method |
| --- | --- |
| User login / session creation | ROLA |
| Prove account ownership to backend | ROLA |
| Transfer assets or call [components](/contents/tech/core-concepts/components) | On-ledger transaction |
| Gate content by [badge](/contents/tech/core-concepts/badges) ownership | ROLA + Gateway query |

ROLA proves _identity_. On-ledger transactions perform _actions_. For token-gated access, verify ownership via ROLA then query the account's [resources](/contents/tech/core-concepts/resources) via the [Gateway SDK](/developers/frontend/02-gateway-sdk).

## Reference Implementation and Maintenance Status (checked October 2026)

ROLA works, and it is still the right way to log a user in. But every official piece around it has stopped receiving updates, and one of them is archived, so check the source before you copy code out of a Radix repository.

- **The examples repository is archived.** [radixdlt/rola-examples](https://github.com/radixdlt/rola-examples) is marked archived on GitHub and its most recent commit is dated 8 September 2023. It pins `@radixdlt/radix-dapp-toolkit` at `0.5.1` and sets `networkId: 13`, an id the [Gateway SDK](https://www.npmjs.com/package/@radixdlt/babylon-gateway-api-sdk)'s own `RadixNetwork` map does not contain at all, since it goes from 12 straight to 14. The example also predates the `@radixdlt/rola` package: its server verifies proofs with a hand-written implementation under `apps/server/src/rola/` rather than the package this page recommends.
- **The package is frozen, and widely installed anyway.** [@radixdlt/rola](https://www.npmjs.com/package/@radixdlt/rola) is at version 2.1.0, published on 20 November 2024, and npm `latest` has not moved since. Its [source repository](https://github.com/radixdlt/rola) has had no code change since then; its one 2026 commit, on 22 January, switched the npm publish step to OIDC. Downloads rose anyway, to 6,283 in September 2026.
- **The toolkit is in the same position.** The dated version of that story is on [Radix dApp Toolkit](/developers/frontend/01-radix-dapp-toolkit): bug fixes only, on a volunteer basis, pending a community decision on who takes the libraries on.

The practical rule for all three: read the archived example to see how the parts connect, then check every method name against the type definitions inside the version you actually installed, because the two do not agree.

## Next Steps

- [dApp Definition and Wallet Verification](/developers/frontend/04-dapp-definition-and-verification) – required before the Radix Wallet will accept your requests on Mainnet

## External Links

- [ROLA documentation](https://docs.radixdlt.com/docs/rola-radix-off-ledger-auth)
- [@radixdlt/rola on npm](https://www.npmjs.com/package/@radixdlt/rola)
- [ROLA examples repository](https://github.com/radixdlt/rola-examples) – archived September 2023, pinned to RDT 0.5.1; read it for the shape, not for the API
