Passkey Demo App with WebAuthn and Ethereum
In the realm of decentralized applications (dApps), user authentication remains a critical aspect. Traditional methods often rely on centralized servers, which can be a point of vulnerability. Enter Passkey: a decentralized authentication method that leverages the power of WebAuthn and Ethereum.
What is Passkey?
Passkey is a concept where users can authenticate themselves using cryptographic keys instead of traditional usernames and passwords. By integrating WebAuthn, a web standard for secure authentication, with Ethereum, a decentralized blockchain platform, Passkey offers a robust and secure authentication mechanism for dApps.
In this guide, we'll walk you through creating a demo app that showcases this integration using create-react-app
.
Before jumping onto the tutorial, let us look at some use cases for a passkey type encryption on the decentralized web. The use cases ranges all the way from:
To more general application like:
Basically anything that needs frequent signing for authentication can make use of Lighthouse Passkey authentication.
Ensure you have Node.js and npm installed. If not, download and install them from Node.js official website.
npx create-react-app passkey-demo
cd passkey-demo
npm install axios
These functions will aid in the authentication process:
const getAuthMessage = async (address) => {
try {
const data = await axios
.get(`https://encryption.lighthouse.storage/api/message/${address}`, {
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.data[0].message);
return { message: data, error: null };
} catch (err) {
return { message: null, error: err?.response?.data || err.message };
}
};
function bufferToBase64url(buffer) {
const byteView = new Uint8Array(buffer);
let str = "";
for (const charCode of byteView) {
str += String.fromCharCode(charCode);
}
// Binary string to base64
const base64String = btoa(str);
// Base64 to base64url
// We assume that the base64url string is well-formed.
const base64urlString = base64String
?.replace(/\+/g, "-")
?.replace(/\//g, "_")
?.replace(/=/g, "");
return base64urlString;
}
function base64urlToBuffer(base64url) {
let binary = atob(base64url?.replace(/_/g, "/")?.replace(/-/g, "+"));
let length = binary.length;
let buffer = new Uint8Array(length);
for (let i = 0; i < length; i++) {
buffer[i] = binary.charCodeAt(i);
}
return buffer;
}
function transformPublicKey(publicKey) {
const selected_key_index = 0;
let transformedPublicKey = {
...publicKey,
challenge: new Uint8Array([...publicKey.challenge.data]),
allowCredentials: [
{
type: "public-key",
id: base64urlToBuffer(
publicKey.allowCredentials[selected_key_index]?.credentialID
),
},
],
};
return [
transformedPublicKey,
publicKey.allowCredentials[selected_key_index]?.credentialID,
];
}
Our main React component will handle user interactions:
import React, { useState } from "react";
import axios from "axios";
import "./App.css";
function App() {
// State variables for account, error, chain ID, keys, and token
const [account, setAccount] = useState("");
const [error, setError] = useState("");
const [chainId, setChainId] = useState("");
const [keys, setKeys] = useState({});
const [token, setToken] = useState("");
// Function to connect to the Ethereum wallet
const connectWallet = async () => {
if (window.ethereum) {
try {
// Request account access
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAccount(accounts[0]);
const chainId = await window.ethereum.request({
method: "eth_chainId",
});
setChainId(chainId);
} catch (error) {
console.error("User denied account access");
}
} else {
console.error("Ethereum provider not detected");
}
};
// Function to disconnect from the Ethereum wallet
const disconnect = () => {
setAccount("");
setChainId("");
};
// Function to sign a message using the Ethereum wallet
const signMessage = async (message) => {
try {
const signature = await window.ethereum.request({
method: "personal_sign",
params: [account, message],
});
return signature;
} catch (error) {
setError(error.toString());
}
};
// Convert account to lowercase for uniformity
const username = account.toLowerCase();
// Function to login using Passkey
const login = async () => {
try {
const startResponse = await axios.post(
"https://encryption.lighthouse.storage/passkey/login/start",
{
address: username,
}
);
const publicKey = startResponse.data;
const [transformedPublicKey, credentialID] = transformPublicKey(publicKey);
// Get credentials using WebAuthn
const credential = await navigator.credentials.get({
publicKey: transformedPublicKey,
});
// Convert credential to a format suitable for the backend
const serializeable = {
authenticatorAttachment: credential.authenticatorAttachment,
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
response: {
attestationObject: bufferToBase64url(credential.response.attestationObject),
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
signature: bufferToBase64url(credential.response.signature),
authenticatorData: bufferToBase64url(credential.response.authenticatorData),
},
type: credential.type,
};
const finishResponse = await axios.post(
"https://encryption.lighthouse.storage/passkey/login/finish",
{
credentialID,
data: credential,
}
);
const token = finishResponse.data.token;
setToken(token);
if (token) {
alert("Successfully authenticated using webAuthn");
}
} catch (error) {
console.error("Error during login:", error);
}
};
// Function to register using Passkey
const register = async () => {
try {
const { message } = await getAuthMessage(account.toLowerCase());
const signedMessage = await signMessage(message);
const response = await axios.post(
"https://encryption.lighthouse.storage/passkey/register/start",
{
address: account.toLowerCase(),
}
);
const publicKey = {
...response.data,
challenge: new Uint8Array([...response.data?.challenge?.data]),
user: {
...response.data?.user,
id: new Uint8Array([...response.data?.user?.id]),
},
};
// Create credentials using WebAuthn
const data = await navigator.credentials.create({ publicKey });
const finishResponse = await axios.post(
"https://encryption.lighthouse.storage/passkey/register/finish",
{
data,
address: username,
signature: signedMessage,
name: "MY Phone",
}
);
const finishData = await finishResponse.data;
if (finishData) {
alert("Successfully registered with WebAuthn");
} else {
throw new Error("Registration was not successful");
}
} catch (error) {
alert(error.message);
}
};
// Function to delete credentials
const deleteCredentials = async () => {
try {
const startResponse = await axios.post(
"https://encryption.lighthouse.storage/passkey/login/start",
{
address: username,
}
);
const publicKey = startResponse.data;
const { message } = await getAuthMessage(account.toLowerCase());
const signedMessage = await signMessage(message);
const response = await axios.delete(
"https://encryption.lighthouse.storage/passkey/delete",
{
data: {
address: account.toLowerCase(),
credentialID: publicKey.allowCredentials[0]?.credentialID,
},
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${signedMessage}`,
},
}
);
} catch (error) {
alert(error.message);
}
};
// Render the app UI
return (
<div className="App">
<header className="App-header">
{!account ? (
<button className="App-link" onClick={connectWallet}>
Connect Wallet
</button>
) : (
<button className="App-link" onClick={disconnect}>
Disconnect
</button>
)}
<p>{`Account: ${account}`}</p>
<p>{`Network ID: ${chainId ? Number(chainId) : "No Network"}`}</p>
<p>
Edit <code>src/App.jsx</code> and save to reload.
</p>
{account && (
<>
<button className="App-link" onClick={register}>
Register
</button>
<button className="App-link" onClick={login}>
Login
</button>
<button className="App-link" onClick={deleteCredentials}>
Delete
</button>
<textarea
style={{ fontWeight: "0.9rem", maxWidth: "80vw" }}
value={`Bearer ${token}`}
></textarea>
</>
)}
</header>
</div>
);
}
Connecting to Ethereum Wallet
connectWallet
Function Explanation:
connectWallet
function is designed to establish a connection with the user's Ethereum wallet.Disconnecting from the Ethereum Wallet
disconnect
Function Explanation:
disconnect
function allows users to sever their connection from the Ethereum wallet.account
and chainId
state variables to their default values, effectively logging the user out of their Ethereum wallet.Signing a Message with Ethereum Wallet
signMessage
Function Explanation:
signMessage
function is crafted to solicit a signature from the user's Ethereum wallet for a specified message.Logging in Using Passkey
login
Function Explanation:
login
function orchestrates the login process leveraging Passkey.Registering with Passkey
register
Function Explanation:
register
function manages the user registration process via Passkey.Deleting Credentials
deleteCredentials
Function Explanation:
deleteCredentials
function facilitates the removal of user credentials from the system.return
Function Explanation:
<div>
element with a class of "App".<header>
element with a class of "App-header".src/App.jsx
file.After setting up the demo app and understanding its various components, it's time to test it out and see the Passkey authentication in action. Here's a step-by-step guide on how to test the demo app:
First, navigate to the root directory of your project in the terminal and run the following command to start the React development server:
npm start
This will automatically open a new browser window/tab with the app running on http://localhost:3000
.
On the app's main page, you'll see a "Connect Wallet" button. Click on it. If you have an Ethereum wallet extension like MetaMask installed, it will prompt you to connect your wallet to the app. Grant permission.
This will open your metamask extension asking for permission to connect. Grant the permission.
Once connected, the app will display your Ethereum account address and the network ID (or chain ID). This confirms that the app has successfully connected to your Ethereum wallet.
Click on the "Register" button. This will initiate the Passkey registration process, which involves:
If the registration is successful, you'll receive an alert saying "Successfully registered with WebAuthn".
After registering, click on the "Login" button. This will authenticate you using the previously registered credentials. Upon successful authentication, you'll receive a token, which will be displayed in
With the above setup, you now have a demo app that showcases the power and security of Passkey authentication. By combining the cryptographic strength of WebAuthn with the decentralized nature of Ethereum, Passkey offers a future-proof solution for dApp authentication. Dive in and explore the next generation of user authentication!
Our Blogs
Read our latest blog
Nandit Mehra
Encryption and Access Control for Web3 using Lighthouse
Lighthouse
How To Migrate Your Files To Lighthouse
Nandit Mehra
Decentralized storage for the Ocean Protocol
Ravish Sharma
Creating a Pay-to-View Model Using Lighthouse Storage
Aryaman Raj
Getting Started with Lighthouse Python SDK
Aryaman Raj
A Comprehensive Guide to Publishing and Updating Content with Lighthouse IPNS
Aryaman Raj, Nandit Mehra
Time Lock Encryption using Lighthouse Access Control
Aryaman Raj
Secure File Sharing using Lighthouse SDK: A Step-by-Step Guide
Aryaman Raj
Passkey Demo App with WebAuthn and Ethereum
Ishika Rathi
Web3 Storage: IPFS and Filecoin Guide
Ishika Rathi
Understanding How web3 storage Operates
Ishika Rathi
Lighthouse: Secure Web3 Storage for Your AI Data
Ishika Rathi
Decentralized Storage: A Smarter, Safer, and Cheaper Way to Manage Your Data
Ishika Rathi
Unveiling the Mechanics of Perpetual Storage
Ishika Rathi
Navigating Permanent Storage: Harnessing the Power of Filecoin and IPFS
Ishika Rathi
Decentralized Excellence: Elevating Data Storage with Lighthouse
Ishika Rathi
Revolutionizing Permanence in Data Storage
Ishika Rathi
Eternalizing Data: A Permanent storage
Ishika Rathi
Exploring Web3 Advancements in Storage Solutions
Ishika Rathi
NFT Storage Strategies
Ishika Rathi
On-Chain Encryption: Security Unveiled
BananaCircle
Web2 Storage Challenges Versus Web3 Solutions Ft. Lighthouse
Nandit Mehra
Discover How the Endowment Pool Makes Your Data Immortal
Nandit Mehra
What is FHE and how Lighthouse plans to use it
Nandit Mehra
AI Meets Blockchain: Beyond the Hype & Into the Future
Nandit Mehra
August at Lighthouse: Milestones & Innovations
Nandit Mehra
September at Lighthouse: Milestones & Innovations
Nandit Mehra
October at Lighthouse: Milestones & Innovations