Similar to the user signatures, there are multiple ways that you can verify (message, signature) pairs.
Resources
Quickstart Repo - This quickstart repository implements Blockin verification in a couple ways (BitBadges API, self-host). You can choose which method depending on your use case.
Pre-Requisites
Consider the setting in which you are verifying and pass in the corresponding options in Step 2 accordingly.
balancesSnapshot is useful for offline asset verification (i.e. no Internet access). Otherwise, ChainDrivers typically require Internet to query balances.
skipTimestampVerification is useful if you are pre-verifying. By default we check that the current time is in between notBefore and expirationDate, but sometimes, you may want to pre-check a sign in will be valid at a certain time.
skipAssetVerification will skip the asset verification altogether. If this is true, Blockin will be fully functioning in an offline environment.
earliestIssuedAt will set an earliest date the issuedAt field of the challenge can be. This can be used to implement time-dependent validity windows. For example, only approve challenges issued in the last minute.
As mentioned in the core concepts, it is critical you verify the message is in the expected format when received from the user. Blockin verifies the message as-is, so a manipulated message will get a manipulated verification response. Consider using the expectedChallengeParams option of verifyChallenge.
If you are using a centralized helper tool, consider also performing additional quality checks on your end, such as verifying signatures manually, checking messages, etc.
Replay Attacks
You need to also implement a replay attack prevention mechanism as well. This can be application dependent. See the Core Concepts - Replay Attacks sections for further examples.
Recommended: Most use cases will be for digital authentication that is instant (sign time -> auth time) with secure communication channels, such as gating a website. If this is applicable to you, we recommend implementing time-dependent windows (e.g. you have 1 minute to "redeem" you sign in). This can be implemented with the earliestIssuedAt option of verifyChallenge. This will check the issuedAt field of the challenge and assert it is recent enough. If it is not, authentication will fail (thus preventing replay attacks after a certain time).
Flash Ownership Attacks
If authenticating with assets, you should be aware of flash ownership attacks. Basically, two sign ins at different times would be approved if the badge is transferred between the time of the first sign in and the second one. See this section for more information.
The next step is to verify with the Blockin library.
Option 1: BitBadges API
The easiest way is to simply use the BitBadges API (requires Internet). All verification logic is handled for you, but you sacrifice customization (you are stuck with whatever ChainDrivers the API implements). This is also a more centralized option.
import { BigIntify, GenericBlockinVerifyRouteSuccessResponse, getChainForAddress, BitBadgesApi } from"bitbadgesjs-sdk";import { constructChallengeObjectFromString } from'blockin'constmessage=...;constsignature=...;//Implement Step 1 here or within expectedChallengeParamsconstchain=getChainForAddress(constructChallengeObjectFromString(message, BigIntify).address);constres=awaitBitBadgesApi.verifySignInGeneric({ message, chain, signature, publicKey, secretsProofs, options: { expectedChallengeParams: { ... } } //See https://blockin-labs.github.io/blockin/docs/types/VerifyChallengeOptions.html});//TODO: Implement your custom logic checks
Option 2: Directly Call Blockin
You can also custom implement and call verifyChallenge directly. See the ChainDrivers documentation for more information on setting ChainDrivers. ChainDrivers can either be custom implemented or imported from pre-built ones. Make sure they align with your application's requirements.
constchainDriver=getChainDriver(req.body.chain);constbody=req.body;try {//Implement Step 1 here or within body.optionsconstverificationResponse=awaitverifyChallenge( chainDriver,body.message,body.signature, (item:NumberType) => { returnBigInt(item) },body.options //See https://blockin-labs.github.io/blockin/docs/types/VerifyChallengeOptions.html );//Post clean checks} //..
Step 3: Custom Application Logic
We leave any additional custom logic up to you. This includes preventing replay attacks, implementing sessions, authenticating with your private database values, max uses per account / badge? specific whitelisted addresses only?, etc.
Example
// TODO: This can be replaced with other session methods, as well.constsessionData= { chain:getChainForAddress(params.address), address:params.address, nonce:params.nonce,};// Set the session cookieres.setHeader('Set-Cookie',cookie.serialize('session',JSON.stringify(sessionData), { httpOnly:true,// Make the cookie accessible only via HTTP (not JavaScript) expires:newDate(params.expirationDate), path:'/',// Set the cookie path to '/' sameSite:'strict',// Specify the SameSite attribute for security secure:process.env.NODE_ENV==='production',// Ensure the cookie is secure in production}));
Putting It Together
This specific example prevents against replay attacks via the issuedAt field.
//frontend/*const res = await verifyAuthenticationAttempt(message, signature, { expectedChallengeParams: { domain: codeGenerationParams.challengeParams.domain, statement: codeGenerationParams.challengeParams.statement, ... }});export const verifyAuthenticationAttempt = async (message: string, sig: string, options?: VerifyChallengeOptions): Promise<GenericBlockinVerifyRouteSuccessResponse> => { const chain = getChainForAddress(constructChallengeObjectFromString(message, BigIntify).address); const verificationRes = await fetch('../api/verifyAuthenticationAttempt', { method: 'post', body: JSON.stringify({ message: message, signature: sig, options, chain }), headers: { 'Content-Type': 'application/json' } }).then(res => res.json()); return verificationRes;}*///backend routeimport { NextApiRequest, NextApiResponse } from"next";import { BitBadgesApi } from"./bitbadges-api";import cookie from'cookie';import { constructChallengeObjectFromString } from"blockin";import { BigIntify } from"bitbadgesjs-proto";import { getChainForAddress } from"bitbadgesjs-utils";constverifyAuthenticationAttempt=async (req:NextApiRequest, res:NextApiResponse) => {constbody=req.body;try {constparams=constructChallengeObjectFromString(body.message, BigIntify);//Option 1: Outsourced constresponse=awaitBitBadgesApi.verifySignInGeneric({ message:body.message, chain:body.chain, signature:body.signature, options:body.options });//Option 2: // const chainDriver = getChainDriver(body.chain);// const challenge: ChallengeParams<NumberType> = constructChallengeObjectFromString(body.message, BigIntify);/* const verificationResponse = await verifyChallenge( chainDriver, body.message, body.signature, BigIntify, body.options ); */if (response.success) {//If success, the Blockin message is verified. This means you now know the signature is valid and any assets specified are owned by the user. //We have also checked that the message parameters match what is expected and were not altered by the user (via body.options.expectedChallengeParams).//TODO: You now implement any additional checks or custom logic for your application, such as assigning sesssions, cookies, etc.//TODO: It is also important to prevent replay attacks.//You can do this by storing the message and signature in a database and checking that it has not been used before. //Or, you can check the signature was recent.//For best practices, they should be one-time use only.if (params.issuedAt &&newDate(params.issuedAt).getTime() <Date.now() -1000*60*5) {returnres.status(400).json({ verified:false, message:'This sign-in is too old' }); } elseif (!params.issuedAt ||!params.expirationDate) {returnres.status(400).json({ verified:false, message:'This sign-in does not have an issuedAt timestamp' }); }// TODO: You can add your logic here to create and set a session cookie. This can be replaced with other session methods, as well.constsessionData= { chain:getChainForAddress(params.address), address:params.address, nonce:params.nonce, };// Set the session cookieres.setHeader('Set-Cookie',cookie.serialize('session',JSON.stringify(sessionData), { httpOnly:true,// Make the cookie accessible only via HTTP (not JavaScript) expires:newDate(params.expirationDate), path:'/',// Set the cookie path to '/' sameSite:'strict',// Specify the SameSite attribute for security secure:process.env.NODE_ENV==='production',// Ensure the cookie is secure in production }));//Once the code reaches here, you should considered the user authenticated.returnres.status(200).json({ verified:true, message:'Successfully verified signature' }); } else {returnres.status(400).json({ verified:false, message:'Blockin verification failedd' }); } } catch (err) {returnres.status(400).json({ verified:false, message:`${err}` }); }};exportdefault verifyAuthenticationAttempt;
Consider outsourcing this to a spreadsheet session manager if your use case allows. See for a Google Sheets template. You will have to implement stuff custom to your application, but the core logic is all in the template. This tool can be used, for example, for scanning a QR code. Use your QR code scanner in keyboard mode and have it type the code (signature) directly in the browser to be verified. Note this tool only works out of the box with (message, signature) pairs stored via the BitBadges API.