🆔Verification

Verification

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.

export type VerifyChallengeOptions = {
  expectedChallengeParams?: Partial<ChallengeParams<NumberType>>;
  balancesSnapshot?: object
  skipTimestampVerification?: boolean,
  skipAssetVerification?: boolean,
  earliestIssuedAt?: string,
}

Step 1: Expected Message + Attack Prevention Checks

Expected Message

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.

//verifyChallenge options
{
  expectedChallengeParams: {
    domain: codeGenerationParams.challengeParams.domain,
    statement: codeGenerationParams.challengeParams.statement,
    uri: codeGenerationParams.challengeParams.uri,
    nonce: codeGenerationParams.challengeParams.nonce,
    expirationDate: codeGenerationParams.challengeParams.expirationDate,
    resources: [],
    assetOwnershipRequirements: { ... }
  },
  earliestIssuedAt: new Date(Date.now() - 60 * 1000).toISOString()
}

Step 2: Verify with Blockin

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'

const message = ...;
const signature = ...;

//Implement Step 1 here or within expectedChallengeParams

const chain = getChainForAddress(constructChallengeObjectFromString(message, BigIntify).address);
const res = await BitBadgesApi.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.

pageSetting Chain Drivers
const chainDriver = getChainDriver(req.body.chain);
const body = req.body;

try {
  //Implement Step 1 here or within body.options
  const verificationResponse = await verifyChallenge(
    chainDriver,
    body.message,
    body.signature,
    (item: NumberType) => { return BigInt(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.
const sessionData = {
  chain: getChainForAddress(params.address),
  address: params.address,
  nonce: params.nonce,
};

// Set the session cookie
res.setHeader('Set-Cookie', cookie.serialize('session', JSON.stringify(sessionData), {
  httpOnly: true, // Make the cookie accessible only via HTTP (not JavaScript)
  expires: new Date(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 route
import { 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";

const verifyAuthenticationAttempt = async (req: NextApiRequest, res: NextApiResponse) => {
  const body = req.body;

  try {
    const params = constructChallengeObjectFromString(body.message, BigIntify);
    //Option 1: Outsourced 
    const response = await BitBadgesApi.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 && new Date(params.issuedAt).getTime() < Date.now() - 1000 * 60 * 5) {
        return res.status(400).json({ verified: false, message: 'This sign-in is too old' });
      } else if (!params.issuedAt || !params.expirationDate) {
        return res.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.
      const sessionData = {
        chain: getChainForAddress(params.address),
        address: params.address,
        nonce: params.nonce,
      };

      // Set the session cookie
      res.setHeader('Set-Cookie', cookie.serialize('session', JSON.stringify(sessionData), {
        httpOnly: true, // Make the cookie accessible only via HTTP (not JavaScript)
        expires: new Date(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.
      return res.status(200).json({ verified: true, message: 'Successfully verified signature' });
    } else {
      return res.status(400).json({ verified: false, message: 'Blockin verification failedd' });
    }
  } catch (err) {
    return res.status(400).json({ verified: false, message: `${err}` });
  }
};

export default verifyAuthenticationAttempt;

Last updated