How To Remove Recovery Codes for Non-Custodial Wallets Using Paper's Embedded Wallet Service
Let’s face it: NFTs are growing in mainstream adoption. As more use cases shift to the blockchain, securing your users becomes increasingly important.
The most reliable way to securely manage your users’ assets is with non-custodial wallets.
And in this guide, we'll show you how to make non-custodial wallets super simple for your users, by removing the need for them to store a recovery code.
What is a non-custodial wallet?
A non-custodial wallet is a digital safe for NFTs and cryptocurrency where only the owner can move or access the stored items. Neither the wallet provider nor the application the user is connected to can access or reconstruct the private key. This means that even if you (as the developer) or Paper (as the wallet provider) get breached, your users’ assets will still be safe.
Last time we showed how the Embedded Wallet Service lets you easily create email-based login for your web3 apps. See our docs for more info on how to set it up and some of its more powerful features.
Why we use recovery codes
If you tried out the Embedded Wallet Service demo, you may have noticed you were emailed a recovery code the first time you signed in.
“What’s the recovery code for?”
When a user signs in to your app on a new device, they need to provide a recovery code to decrypt their private key and access their wallet.
“But that is too much friction!”
You’re right, and we’re on the same page! How can we bring the familiar and simple UX of web2 experiences to non-custodial wallets?
An example implementation
Here’s a minimal code snippet to sign in with the Embedded Wallet SDK.
I’m using React but the package works on any JS framework.
// Initialize the SDK early to load asynchronously and improve performance.
const sdk = new PaperEmbeddedWalletSdk({
chain: 'Mumbai',
clientId: 'YOUR_CLIENT_ID',
});
const LoginWithPaperButton = () => {
const [email, setEmail] = useState('');
const onClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
try {
const { user } = await sdk.auth.loginWithPaperEmailOtp({
email,
});
} catch (e) {
// The user closed the login modal.
}
};
return (
<>
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value.trim())}
/>
<button type='submit' onClick={onClick}>
Login
</button>
</>
);
};
After pressing Login a nice modal pops open prompting the user for their OTP code.
Currently, when a user logs in on a new device, they will be prompted for their recovery code:
Removing the recovery code step
tl;dr: You store each user’s recovery code in your database and provide it to the SDK on every login.
const maybeGetRecoveryCode = async (email: string): Promise<string | undefined> => {
// This method retrieves the user's recovery code from your backend.
// This code may not exist if the user has not signed in before.
}
const saveRecoveryCode = async (email: string, recoveryCode: string): Promise<void> => {
// This method stores the user's recovery code on your backend.
}
const onClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
try {
// Get the user's recovery code, if found.
const savedRecoveryCode: string | undefined = await maybeGetRecoveryCode(email);
const { user } = await sdk.auth.loginWithPaperEmailOtp({
email,
recoveryCode: savedRecoveryCode,
});
// Save the user's recovery code after their first login on each device.
if (!savedRecoveryCode && user.authDetails.recoveryCode) {
await saveRecoveryCode(email, user.authDetails.recoveryCode);
}
} catch (e) {
// The user closed the login modal.
}
};
We added a few new pieces:
maybeGetRecoveryCode
- You’ll provide this method to retrieve a recovery code from your backend database. The recovery code may be undefined for new users.saveRecoveryCode
- You’ll provide this method to save the recovery code to your backend database to be retrieved in the future.- Provide the
savedRecoveryCode
tologinWithPaperEmailOtp()
- If the auth flow requires a recovery code, it will use this code. If a recovery code is not required, this argument is ignored. An exception will be thrown if the recovery code is incorrect.
🚀 And that’s it! The next time a user signs in on a new device, they won’t be prompted for their recovery code.
Great job making it this far!
FAQ
Is this approach still secure?
Yes, all the crucial security requirements of Embedded Wallets are still met:
- Paper can never reconstruct a user's private key.
- You (the app developer) can never reconstruct a user's private key.
Can my users still sign into Paper’s Wallet page?
Yes, the recovery code is still emailed to users so they can provide it when prompted on https://withpaper.com/wallet. This page allows them to manage their NFTs and connect to web3 apps.
Summary
The Embedded Wallet Service allows users to sign in to a non-custodial wallet using just their email - no passwords or recovery codes are needed.
It takes just a few steps to skip recovery codes in your app already set up with Embedded Wallets:
- Retrieve the recovery code from your backend and provide it to the SDK when a user signs in.
- Store the recovery code on your backend after a user signs in.
For you visual learners: Previously, the user provided the recovery code when signing into a new device.
Now your app backend provides the recovery code, greatly simplifying the sign-in flow.
Embedded Wallets combines a delightful user onboarding experience with peace-of-mind security. Get started building today or learn more.