The Sign-In functionality for the JustaName Widget can be implemented using two approaches: session-based authentication or JWT (JSON Web Token) authentication. Both methods provide secure ways for users to sign in with their ENS or wallet address. Below are implementations for each method.
Install JustaName SDK
Install the JustaName SDK in your backend by running the following command:
npminstall@justaname.id/sdk
pnpminstall@justaname.id/sdk
yarnadd@justaname.id/sdk
Sign-In with Sessions
In this approach, express-session is used to handle user sessions. The session data is stored on the server, and the user's sign-in information (such as their ENS and wallet address) is persisted throughout their session.
This setup can also be achieved using serverless functions, such as AWS Lambda, Google Cloud Functions, or Vercel Functions. Simply deploy the backend logic to your serverless environment and configure your backendUrl in the Widget to point to the appropriate endpoint.
In this approach, JWT is used to manage user authentication. After the user signs in, a JWT is issued, containing the user's ENS and wallet address. This token is stored in an HttpOnly cookie, and subsequent requests authenticate the user via the JWT.
Example: JWT-Based Sign-In
import express, { Request } from'express';import cors from'cors';import { JustaName, ChainId, JustaNameConfig } from"@justaname.id/sdk";import dotenv from'dotenv';import jwt from'jsonwebtoken';import cookieParser from'cookie-parser';dotenv.config();constchainId=parseInt(process.env.JUSTANAME_CHAIN_IDasstring) asChainId;constdomain=process.env.JUSTANAME_DOMAINasstring;constorigin=process.env.JUSTANAME_ORIGINasstring;constapiKey=process.env.JUSTANAME_API_KEYasstring;constproviderUrl=process.env.JUSTANAME_PROVIDER_URLasstring;constensDomain=process.env.JUSTANAME_ENS_DOMAINasstring;exportconstconfig:JustaNameConfig= { apiKey, providerUrl: providerUrl, config:{ chainId, domain, origin, }, ensDomain}constapp=express();app.use(cors({ origin:true, credentials:true, }));app.use(express.json());app.use(cookieParser());let justaname:JustaName;constJWT_SECRET=process.env.JWT_SECRET||'your_jwt_secret';// Middleware to authenticate JWT from cookiefunctionauthenticateToken(req:Request, res, next) {consttoken=req.cookies['justverifiedtoken'];if (!token) {returnres.status(401).json({ message:'You have to first sign in' }); }jwt.verify(token,JWT_SECRET, (err, user) => {if (err) {returnres.status(403).json({ message:'Invalid token' }); }req.user = user; // Save user data in requestnext(); });}// Endpoint to generate nonce for sign-inapp.get('/api/signin/nonce',async (req:Request, res) => {constnonce=justaname.signIn.generateNonce();consttoken=jwt.sign({ nonce },JWT_SECRET, { expiresIn:'1h' });res.cookie('justverifiednonce', token, { httpOnly:true, secure:true, sameSite:'None', });res.status(200).send(nonce);});// Endpoint to handle sign-in using JWTapp.post('/api/signin',async (req:Request, res) => {try {const { message,signature } =req.body;const { data: messageData,ens } =awaitjustaname.signIn.signIn({ message, signature, });constpayload= { address:messageData.address, ens, chainId:messageData.chainId, };constexpiresIn=Math.floor( (newDate(messageData.expirationTime).getTime() -Date.now()) /1000 );consttoken=jwt.sign(payload,JWT_SECRET, { expiresIn });res.cookie('justverifiedtoken', token, { httpOnly:true, secure:true, sameSite:'None', });returnres.status(200).send({ ens, address:messageData.address, chainId:messageData.chainId }); } catch (error) {res.status(500).json({ message:error.message }); }});// Endpoint to retrieve current signed-in user details using JWTapp.get('/api/current', authenticateToken, (req:Request, res) => {res.status(200).json({ ens:req.user.ens, address:req.user.address, chainId:req.user.chainId, });});// Endpoint to sign out and clear the JWTapp.post('/api/signout', (req, res) => {res.clearCookie('justverifiedtoken', { httpOnly:true, secure:true, sameSite:'None', });res.status(200).json({ message:'You have been signed out' });});constport=process.env.PORT||3333;constserver=app.listen(port,async () => { justaname =JustaName.init(config);console.log(`Listening at http://localhost:${port}/api`);});server.on('error',console.error);
Backend Explanation:
Nonce Generation: The /api/signin/nonce endpoint generates a unique nonce for the user, which is used to verify the user's identity when they sign in.
User Authentication: The /api/signin endpoint verifies the user's signature using the nonce and their wallet. Once the signature is validated, the user's session is established, either using sessions or JWT tokens, depending on the implementation. The backend securely handles the entire authentication process to prevent any exposure of sensitive data on the frontend.
Session or JWT Token Handling: After successful authentication, the backend creates either a session or issues a JWT token to manage the user's authentication state. This session or token is stored securely (e.g., in cookies) and used to authenticate subsequent requests.
Security: User authentication and session management are handled entirely on the backend, ensuring that sensitive operations such as nonce generation and signature verification are not exposed to the frontend, maintaining a high level of security