Non-custody 2/2: customer login and delegated wallets

Give your users ownership and power to use their wallets directly.

For this example we will use Dfns typescript SDK to register end users (your customers) to the Dfns API and let them own their wallets and use it directly.

In this scenario, the wallets live in your organization but belong to your end users. Only they can do transactions and use the private key to sign messages. All actions require to be signed by their passkey. The SDK greatly simplifies that process.

1

Clone the example

This example contains all the functions you need to get started with login and wallets delegation.

git clone https://github.com/dfns/dfns-sdk-ts.git --no-checkout
cd dfns-sdk-ts
git sparse-checkout set examples/sdk/nextjs-delegated
git checkout m
cd examples/sdk/nextjs-delegated/

Edit next.config.ts to remove the line:

line to remove from next.config.ts:
  transpilePackages: ['@dfns/sdk-browser'],

Update the hardcoded dependencies and install the project:

npm i && npm remove @dfns/sdk @dfns/sdk-browser @dfns/sdk-keysigner && npm i @dfns/sdk @dfns/sdk-browser @dfns/sdk-keysigner
2

Prepare the environment

You can follow the README instructions. For convenience the steps are gathered here.

Copy .env.example to a new file .env.local and set the following values,

  • DFNS_API_URL: https://api.dfns.io or .ninja depending on the environment you are using

  • DFNS_ORG_ID: your Organization ID (found in the Dashboard: click you email then "Account")

  • DFNS_CRED_ID: the Signing Key Cred ID created when you registered the service account. On the dashboard head to Settings > Service Accounts to copy it.

  • DFNS_PRIVATE_KEY: the private key from the step 'generate a keypair', the newlines should not be a problem

  • DFNS_AUTH_TOKEN: the authToken from above, the value should start with eyJ0...

  • NEXT_PUBLIC_PASSKEYS_RELYING_PARTY_ID: the passkey relying party id, aka, the domain where your app lives (Read more here). We advise using the root domain (eg. acme.com, not app.acme.com) for more passkey flexibility (so that passkey is re-usable on subdomains). During development on localhost, you can set it to localhost.

  • NEXT_PUBLIC_PASSKEYS_RELYING_PARTY_NAME: A string representing the name of the relying party, aka, your company name (e.g. "Acme"). The user will be presented with that name when creating or using a passkey.

Run the development server

npm run dev

And finally open http://localhost:3000

3

Service account action signing

As any user on Dfns, your service account needs to sign its actions. The file app/api/clients.ts uses the Dfns SDK to register the service account private key into a signer, as well as a API client that will take care of gathering the right information and requesting signing when necessary.

[Backend] app/api/clients.ts
export const apiClient = (authToken?: string) => {
  const signer = new AsymmetricKeySigner({
    credId: process.env.DFNS_CRED_ID!,
    privateKey: process.env.DFNS_PRIVATE_KEY!.replace(/\\n/g, '\n'),
  })

  return new DfnsApiClient({
    orgId: process.env.DFNS_ORG_ID!,
    authToken: authToken ?? process.env.DFNS_AUTH_TOKEN!,
    baseUrl: process.env.DFNS_API_URL!,
    signer,
  })
}

4

Delegated registration

The service account can use Delegated Registration to register an end user (a.k.a. one of your customers) to Dfns. Registering this user to your platform and validating his login is out of scope here, we just consider that you have properly authenticated your user before creating his Dfns account. The flow is similar to users registration:

  1. Requesting a challenge from Dfns. Note that the username comes from the frontend in this example, but it doesn't have to, you could be providing it directly from your backend.

[Backend] app/api/register/init/route.ts
  const client = apiClient()
  const challenge = await client.auth.createDelegatedRegistrationChallenge({
    body: { kind: 'EndUser', email: username },
  })
  1. Asking the customer to create a new credentials and sign the challenge with it. This is done via the web front end:

[Frontend] app/register/page.tsx
import { WebAuthnSigner } from '@dfns/sdk-browser'
[...]
const attestation = await webauthn.create(challenge)
  1. Registering the end user credentials

Note that you can directly create a delegated wallet directly during registration.

[Backend] app/api/register/complete/route.ts
  const client = apiClient(temporaryAuthenticationToken)
  const registration = await client.auth.registerEndUser({
    body: {
      ...signedChallenge,
      wallets: [{ network: 'EthereumSepolia' }],
    },
  })

5

Delegated login

In a similar flow, once you have authenticated your user on your platform, you can log him into Dfns in order to let him use his wallet.

[Backend] app/api/login/route.ts
  const client = apiClient()
  const login = await client.auth.delegatedLogin({ body: { username } })

You will get a token back from this call, that you can later use in all for all delegated actions.

The user token will allow the user to call the Dfns API directly. Make sure you only use it from your backend and don't share it with the frontend to make sure no one can obtain that token. That's particularly important if you need to control your users actions.

6

Delegated calls to the API

The SDK provides an easy way to call the API with your delegated end user credentials:

[Backend] app/api/clients.ts
export const delegatedClient = (authToken: string) => {
  return new DfnsDelegatedApiClient({
    orgId: process.env.DFNS_ORG_ID!,
    authToken,
    baseUrl: process.env.DFNS_API_URL!,
  })
}

The API requires the end user to sign any modifying action with his passkey. For instance when requesting Dfns to issue a signature using his wallet:

  1. Request a challenge from Dfns. Note that the username comes from the frontend in this example, but it doesn't have to, you could be providing it directly from your backend.

[Backend] app/api/wallets/signatures/init/route.ts
const client = delegatedClient(authToken) // end user token here
[...]
const challenge = await client.wallets.generateSignatureInit({ walletId, body })
  1. Asking the customer to create a new credentials and sign the challenge with it. This is done via the web front end:

[Frontend] app/wallets/page.tsx
import { WebAuthnSigner } from '@dfns/sdk-browser'
[...]
const attestation = await webauthn.sign(challenge)
  1. Finally calling the signature API to trigger the action.

[Backend] app/api/register/complete/route.ts
const client = delegatedClient(authToken)
[...]
const signature = await client.wallets.generateSignatureComplete(
    {
      walletId,
      body: requestBody,
    },
    signedChallenge
  )

7

Going further

Head to the SDK docs to better understand:

More information about the other SDKs: Dfns SDKs

This is the end the Getting Started wizard.

Onboarding to Dfns

Last updated