Core Concepts

Below is a more technical explanation that goes behind the scenes for developers. If you are only looking for a high level overview, we refer you to here.

Execution Flow

Below, we describe the typical flow for the frontend (or user-side) and backend (resource-side).

Out of Scope

The Blockin library does not handle signing the challenges and will never ask for or use your private key. It only uses public blockchain queries and cryptographic signatures to verify. Signing challenges should be implemented separately (by wallets). See examples via our quickstart repo or the BitBadges official frontend.

Challenge Generation (User-Facing)

Who creates / signs the challenge? Each time a user would like to sign-in, they will have to sign a generated challenge using their private key. The challenge can be generated by the user or resource provider. Typically, it is generated by the resource provider, and the user can possibly edit certain fields before sending it back.

Once the challenge is generated, the user must sign it using their crypto wallet (this is not handled by Blockin itself but we provide example implementatons). Once signed, this (challenge, signature) pair is then sent to the resource provider's backend.

What does the challenge look like? The challenge is defined using the EIP-4361 Sign-In With Ethereum interface (also see https://login.xyz/) extended to support a) specifying assets (such as NFTs or badges) and b) each chain's native signature algorithm. The challenge will define everything in plaintext specific to their sign-in attempt such as expiration date, a statement, URIs, chain IDs, nonces, etc.

https://bitbadges.io wants you to sign in with your Ethereum account:
0xb48B65D09aaCe9d3EBDE4De409Ef18556eb53085

Sign this message only if prompted by a trusted party. The signature of this message can be used to authenticate you on BitBadges. By signing, you agree to the BitBadges privacy policy and terms of service.

URI: https://bitbadges.io
Version: 1
Chain ID: 1
Nonce: cPW1vKj0xfTFlrUab
Issued At: 2024-01-21T18:35:21.141Z
Expiration Time: 2024-02-04T16:11:29.880Z
Resources:
Asset Ownership Requirements:
- Requirement A1-1 (satisfied if one of B1 is satisfied):
  - Requirement B1-1:
      Chain: BitBadges
      Collection ID: 1
      Asset IDs: 8 to 8
      Ownership Time: Authentication Time
      Ownership Amount: x1

  - Requirement B1-2:
      Chain: BitBadges
      Collection ID: 1
      Asset IDs: 9 to 9
      Ownership Time: Authentication Time
      Ownership Amount: x1

Challenge Creation Options

Challenges should be created by calling Blockin's createChallenge. This can either be directly, via a UI component that calls it, or via a helper tool that calls it.

Challenge Verification (Backend)

Upon receiving the (challenge, signature) pair from the user, the resource provider's backend should perform the following steps. Again like the frontend, this can either be directly or outsourced via helper tools.

  1. Perform validity checks on the challenge message. Even if you, the provider, generates the challenge, you are expected to verify that the returned challenge was not edited in any undesired way by the user. Blockin verifications are confined to the message definition. If a message is manipulated, Blockin will verify the manipulated message.

  2. Call Blockin's verifyChallenge. This enforces 1) challenge is well-formed according to Blockin standards, 2) challenge was signed correctly by specified address, and 3) if any assets were specified, assert that the requested address satisfies the ownership requirements for their wallet using public blockchain queries. This uses the ChainDriver interface to dynamically perform blockchain specific logic.

  3. Perform any other validity checks not checked by Blockin's library, such as validating nonces, assert requested data for user is correct in their private database, etc. You may find the options parameter of verifyChallenge helpful

  4. Grant the user access, if successful, using preferred method (session tokens, JWTs, cookies, etc).

  5. Return the result to the user and perform any necessary actions on their end (such as frontend UI changes).

Security - Replay Attacks

What are replay attacks?

Preventing replay attacks is crucial in any authentication or authorization system. A replay attack occurs when an attacker intercepts and maliciously reuses a valid communication or transaction between two parties. In the context of Blockin, this could involve intercepting and reusing a valid authentication request (signature / code), compromising the security of the authentication process.

Lets say Bob authenticates at Event A, but his signature / code is maliciously intercepted. The adversary could now theoretically authenticate as Bob at Event A. Thus, the adversary could not replay the signature to be authenticated. It will be caught as already used.

Also, note that the same message signed by the same address will ALWAYS produce the same signature. Thus, it is important for your authentication message to have some sort of randomness to produce unique signatures.

Lets say Event B uses the same authentication message at Event A. Bob authenticates at Event A as intended. However, the authentication provider of Event A now has Bob's signature and could theoretically use it to authenticate at Event B as Bob. Even if the authentication is one-time use only at both Event A and Event B, it could be replayed across events.

How can you protect?

Please make sure you protect accordingly. As mentioned above, you should assume the message may have been manipulated by the user before being returned to you. This means that a replay attack mechanism must consider this as well.

Below are some ways to protect:

  • Restrict and check one use per session per address

  • If using assets, restrict and check one use per session per asset

  • Unique nonce generation: Including a unique and random nonce (number used once) in each challenge to ensure that each request is unique, one time use only, and cannot be replayed. Nonces should be marked as used / not used by you. The generation scheme is left up to you.

  • Time-dependent windows: Consider implementing a small time window where the sign in can be "redeemed". This is different from the authenticated times. For example, you have 1 minute to "redeem" your sign in after it is signed. After that, it is invalid, thus preventing future replay attacks. However, replay attacks during the time window are still possible. This may not be applicable for some applications.

    • These can be implemented with issuedAt timestamps, system times, using recent block hashes, or any time-dependent property.

    • This is typically fine for digital applications with secure, encrypted network communication

Security - Flash Ownership Attacks

If you are authenticating with assets (e.g. verify Bob owns this asset at sign-in time), you need to have protection against what we call flash ownership attacks. This attack is where, for example, Bob signs in with Asset A and immediately transfers Asset A to Alice who then also signs in successfully with Asset A. Two sign ins were approved for Asset A when only one should have been.

Solutions can vary dependent on the application, but here are some ideas:

  • Assert that the asset cannot be transferred on-chain. This can be by making it completely non-transferable or only transferable in desired ways (such as by a trusted entity).

  • If assets are non-fungible, consider preventing two sign ins with the same badge

Note that for chains that support ownership times (such as BitBadges), this is not adequate since ownership times can be transferred. For example Bob signs in with Asset A (Monday - Wednesday) but then transfers the badge + rights from Monday - Wednesday to Alice.

Chain Drivers

Blockin is built around the ChainDriver interface. This is what allows Blockin to support any blockchain dynamically. If you call a Blockin library function that requires some functionality that is chain-specific and not universal, the Blockin library will use the ChainDriver's logic. ChainDrivers are typically only needed on your backend or resource side.

ChainDrivers can have varying implementations for the same blockchain offering different pros and cons. For example, one Ethereum chain driver can be implemented to call the OpenSea API for NFT verification whereas another can verify with an Ethereum node that you are running. Or, one can be setup for offline verification from a balances snapshot.

import EthDriver from './EthDriver';
import CosmosDriver from './CosmosDriver';
import SolDriver from './SolDriver';
import BtcDriver from './BtcDriver';

const ethDriver = new EthDriver('0x1', undefined);
const solDriver = new SolDriver('');
const cosmosDriver = new CosmosDriver('bitbadges_1-1');

export const getChainDriver = (chain: string) => {
  switch (chain) {
    case 'Cosmos':
      return cosmosDriver;
    case 'Ethereum':
      return ethDriver;
    case 'Solana':
      return solDriver;
    default:
      return ethDriver;
  }
}

//getChainDriver(...).verifySignature(...); //Use one of the driver's functions

Offline Verification

Blockin can be implemented for offline-only use cases as well! Signatures and verification of them are simply cryptographic algorithms which can be done offline.

Asset verification can either be skipped in an offline setting or can be done by providing a balancesSnapshot to the verifyChallenge function options.

Hybrid dApps

A popular architecture is what we term a hybrid dApp, where an app will allow users to sign in with a username / password (or another Web2 method) as well as with Blockin / Web3. Apps which only use the crypto address as a username (never for any blockchain transaction) are great candidates to be hybrid dApps.

For compatibility, hybrid dApps can abstract everything behind the scenes to a web3 address (e.g. username @bob123 -> 0xabc123) and handle everything for the user with a managed private key for Blockin compatibility. Instead of requesting a signature, the app will handle all signatures for the user behind the scenes with the managed private key. These signatures are compatible with the entire Blockin interface, so the result can be used to authenticate the user digitally or in-person (make a QR code of the signature) as explained elsewhere in this documentation.

Because everything is managed, we envision the mapped addresses are scoped to the application and only used by that app. Other add-ons may include mapping $USD -> native cryptocurrency for the address behind the scenes or designing in a way that supports migrating to a self-hosted address. Like the rest of Blockin, this is completely customizable!

It is important to note that handling Web2 authentication is still critical. Only provide signatures for a user who has authenticated properly, and make sure it signs for the correct mapped address. And obviously, keep the seed phrase / private key safe and secret.

To implement signing logic, there are plenty of resources. Each blockchain will have its own utility library with a sign function that takes in a private key. Each ChainDriver will have corresponding documentation. You can also see other Blockin full-stack applications to see signatures.

Example

For example, lets say you are hosting a concert and offer tickets as blockchain tokens. You could allow those comfortable with Web3 to claim their ticket with their wallet, or you could handle everything for users who aren't comfortable behind the scenes using a mapped private key / address pair. This also takes out the signature step for the user which provides better user experience.

Ownership Times

The default asset verification is to verify ownership at sign-in time. This will be the most typical use case. However, we also make it easy to verify ownership at specific times in the past or future. This can be done by selecting or implementing a ChainDriver that supports time-based verification in verifyAssets.

The actual implementation can be done a few ways:

  • Query the blockchain's state at that time

  • Provide a balances snapshot manually for that time

  • If the asset natively supports time-dependent balances (such as BitBadges), you can query that.

    • Note if you choose this option, you have to choose if you want to query if the user has ownership rights for time XYZ currently or had rights at time XYZ for time XYZ. Make sure you know which one your selected ChainDrivers do.

AND / OR / NOT Asset Requirements

Blockin supports sign ins with dynamic ownership logic (AND / OR). The assetOwnershipRequirements field is of type AssetConditionGroup. This allows you to do any of the following to apply and / or logic respectively. NOT logic can be applied with mustOwnAmounts set to must own none.

assetOwnershipRequirements: {
  assets: [{
    chain: 'BitBadges',
    collectionId: 1,
    assetIds: [{ start: 9, end: 9 }],
    mustOwnAmounts: { start: 0, end: 0 },
    ownershipTimes: [{ start: 3, end: 3 }],
  }, ...]
}
assetOwnershipRequirements: {
    $and: [
      {
        assets: [...]
        options: { ... }
      },
      {
        assets: [...],
        options: { ... },
    ]
  },
}
assetOwnershipRequirements: {
      $or: [
        {
          assets: [{
            ...
          }, {
            ...
          }],
          options: {
            ...
          }
        },
        {
          assets: [{
            ...
          }],
          options: {
            ...
          }
        },
      ]
    },
  },
export type AssetConditionGroup<T extends NumberType> = AndGroup<T> | OrGroup<T> | OwnershipRequirements<T>;

export interface AndGroup<T extends NumberType> {
  $and: AssetConditionGroup<T>[];
}

export interface OrGroup<T extends NumberType> {
  $or: AssetConditionGroup<T>[];
}

export interface AssetDetails<T extends NumberType> {
  chain: string,
  collectionId: T | string,
  assetIds: (string | UintRange<T>)[],
  ownershipTimes?: UintRange<T>[],
  mustOwnAmounts: UintRange<T>,
  additionalCriteria?: string,
}

export interface OwnershipRequirements<T extends NumberType> {
  assets: AssetDetails<T>[],
  options?: {
    numMatchesForVerification?: T, //0 or undefined = must satisfy all
  }
}

Asset Creation

Assets are not created using the Blockin library. They may be created in any way as long as one can verify ownership on-chain.

BitBadges Integration

BitBadges is a blockchain protocol built on Cosmos SDK to enable the cross-chain issuance of digital blockchain tokens (badges). Badges and authorization go hand-in-hand (e.g. offer premium features to authenticated and verified badge holders).

BitBadges and Blockin are especially compatible since they both were created for the same purpose (one interface that supports users from all chains).

BitBadges is built with Cosmos SDK but is signature compatible with users from multiple blockchains, meaning an Ethereum user or a Cosmos user can own the same badge. Thus, to integrate with BitBadges, simply have a user sign and authenticate via Blockin with their native blockchain of choice. Then, you can verify ownership of badges by querying the BitBadges API / blockchain for that respective user's address from their native chain.

See the full BitBadges documentation here. See quickstart repo for example integrations.

For the sake of assets, they should be defined using the BitBadges UintRange logic as follows. Certain ChainDrivers may also support checking BitBadges lists features. For this, we abstract a list as a unique collection which users have a balance of x1 if they are on the list and x0 if not. Collection ID should be "BitBadges Lists" and the asset ID should be the list ID.

assets: [{
  chain: 'BitBadges',
  collectionId: 1,
  assetIds: [{ start: 9, end: 9 }],
  mustOwnAmounts: { start: 0, end: 0 },
}]

assets: [{
  chain: 'BitBadges',
  collectionId: 'BitBadges Lists',
  assetIds: ["LIST_ID"],
  mustOwnAmounts: { start: 0, end: 0 },
}]

Last updated