Integrating User Credentialing with Rebase

This section integrates the functionality to generate credentials via Rebase.

A completed version of this part can be found in the example repository (03_rebase).

This step will show how to integrate SSX with Rebase. Rebase is a library for building credentialing witnesses and clients to interact with them. It is built on top of our core libraries such as ssi and uses DIDs and Verifiable Credentials (VCs).

In this step, you must install Rebase as a new dependency, as it is not yet supported by SSX. Run the following to add the dependency and create the new component file:

npm i @spruceid/rebase-client@^0.16.2 ethers@5.7.2

You can create the folders and files by yourself or by running the bash command below:

mkdir utils
touch utils/rebase.ts \
      components/RebaseCredentialComponent.tsx

You need to update the NextConfig to support WebAssembly. Add the following to my-app/next.config.js file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    config.externals.push({
      'utf-8-validate': 'commonjs utf-8-validate',
      'bufferutil': 'commonjs bufferutil',
    });
    config.experiments.asyncWebAssembly = true;
    return config
  },
}

module.exports = nextConfig

Add the following to my-app/utils/rebase.ts to add some util methods of Rebase to the project:

export interface BasicPostCredential {
  type: "BasicPostAttestation", // The URN of the UUID of the credential.
  id: string, // The DID of the user who is the credential subject, comes from the VC.credentialSubject.id
  subject: string,
  title: string,
  body: string,
}

export const encode = (c: string): string => {
  return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
};

export const parseJWT = (jwt_str: string): any => {
  const v = jwt_str.split('.');
  if (v.length !== 3) throw new Error('Invalid JWT format');
  const u = v[1];
  const b64 = u.replace(/-/g, '+').replace(/_/g, '/');
  const encoded = atob(b64).split('').map(encode).join('');
  const json_str = decodeURIComponent(encoded);
  return JSON.parse(json_str);
};

export const toCredentialContent = (jwt_str: string): Record<string, any> | void => {
  const parsed = parseJWT(jwt_str);
  const vc = parsed?.vc;
  if (!vc) throw new Error('Malformed jwt, no vc property');
  const t = vc?.type;
  if (!t) throw new Error('Malformed credential, no type property');
  if (t.length !== 2) throw new Error('Malformed credential, type property did not have length of 2');
  const credType = t[1];
  const credID = vc?.id;
  if (!credID) throw new Error('No id property found under vc property in JWT credential');
  const subjID = vc?.credentialSubject?.id;
  if (!subjID) throw new Error('No id property found under vc.credentialSubject property in JWT credential');
  const issuanceDate = vc?.issuanceDate;
  if (!issuanceDate) throw new Error('No issuanceDate property found under vc property in JWT credential');
  const c = {
    type: credType,
    id: credID,
    subject: subjID,
    issuanceDate
  };
  switch (credType) {
    case "BasicPostAttestation": {
      let next = {
        title: getCredSubjProp("title", vc),
        body: getCredSubjProp("body", vc)
      };
      try {
        let reply_to = getCredSubjProp("reply_to", vc);
        next = Object.assign({}, next, { reply_to });
      } catch (_e) { }
      return Object.assign({}, c, next) as BasicPostCredential;
    }
    default:
      return vc;
  }
};

export const toCredentialEntry = (jwt_str: string): Record<string, any> => {
  const content = toCredentialContent(jwt_str);
  return { jwt: jwt_str, content: content };
};

const getCredSubjProp = (prop: string, vc: any): any => {
  let x = vc?.credentialSubject[prop];
  if (!x) throw new Error(`No ${prop} property found under vc.credentialSubject property in JWT credential`)
  return x;
}

Then, add the following into my-app/components/RebaseCredentialComponent.tsx:

"use client";
import { toCredentialEntry } from "@/utils/rebase";
import { SSX } from "@spruceid/ssx";
import { ethers } from "ethers";
import { useEffect, useState } from "react";
import { defaultClientConfig, type Types } from '@spruceid/rebase-client';
import type { AttestationProof, AttestationStatement } from '@spruceid/rebase-client/bindings';

interface IRebaseCredentialComponent {
  ssx: SSX;
}

const RebaseCredentialComponent = ({ ssx }: IRebaseCredentialComponent) => {
  const [rebaseClient, setRebaseClient] = useState<any>();
  const [signer, setSigner] = useState<ethers.Signer>();
  const [title, setTitle] = useState<string>('');
  const [body, setBody] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);
  const [credentialList, setCredentialList] = useState<Array<string>>([]);
  const [viewingContent, setViewingContent] = useState<string | null>(null);

  useEffect(() => {
    getContentList();
    createClient();
    createSigner();
  }, []);

  const getContentList = async () => {
    setLoading(true);
    let { data } = await ssx.storage.list();
    data = data.filter((d: string) => d.includes('/credentials/'))
    setCredentialList(data);
    setLoading(false);
  };

  const createClient = async () => {
    const Client = (await import('@spruceid/rebase-client')).Client;
    const WasmClient = (await import('@spruceid/rebase-client/wasm')).WasmClient;
    setRebaseClient(new Client(new WasmClient(JSON.stringify(defaultClientConfig()))))
  };

  const createSigner = async () => {
    const ethSigner = await ssx.getSigner();
    setSigner(ethSigner);
  };

  const toSubject = () => {
    return {
      pkh: {
        eip155: {
          address: ssx.address(),
          chain_id: '1'
        }
      }
    }
  };

  const sanityCheck = () => {
    if (!rebaseClient) throw new Error('Rebase client is not configured');
    if (!signer) throw new Error('Signer is not connected');
  };

  const statement = async (credentialType: Types.AttestationTypes, content: any): Promise<string> => {
    sanityCheck();
    const o = {};
    (o as any)[credentialType] = Object.assign({ subject: toSubject() }, content);
    const req: Types.Statements = {
      Attestation: o as AttestationStatement
    };
    const resp = await rebaseClient?.statement(req);
    if (!resp?.statement) {
      throw new Error('No statement found in witness response');
    }
    return resp.statement;
  };

  const witness = async (
    credentialType: Types.AttestationTypes,
    content: any,
    signature: string
  ): Promise<string> => {
    sanityCheck();
    const o = {};
    (o as any)[credentialType] = {
      signature,
      statement: Object.assign({ subject: toSubject() }, content)
    };
    const req: Types.Proofs = {
      Attestation: o as AttestationProof
    };
    const resp = await rebaseClient?.witness_jwt(req);
    if (!resp?.jwt) {
      throw new Error('No jwt found in witness response');
    }
    return resp.jwt;
  };

  const issue = async () => {
    setLoading(true);
    try {
      const fileName = 'credentials/post_' + Date.now();
      const credentialType = 'BasicPostAttestation';
      const content = {
        title,
        body
      }
      const stmt = await statement(credentialType, content);
      const sig = (await signer?.signMessage(stmt)) ?? '';
      const jwt_str = await witness(credentialType, content, sig);
      await ssx.storage.put(fileName, jwt_str);
      setCredentialList((prevList) => [...prevList, `my-app/${fileName}`]);
    } catch (e) {
      console.error(e);
    }
    setLoading(false);
  };

  const handleGetContent = async (content: string) => {
    setLoading(true);
    try {
      const contentName = content.replace('my-app/', '')
      const { data } = await ssx.storage.get(contentName);
      setViewingContent(`${content}:\n${JSON.stringify(toCredentialEntry(data), null, 2)}`);
    } catch (e) {
      console.error(e);
    }
    setLoading(false);
  };

  const handleDeleteContent = async (content: string) => {
    setLoading(true);
    const contentName = content.replace('my-app/', '')
    await ssx.storage.delete(contentName);
    setCredentialList((prevList) => prevList.filter((c) => c !== content));
    setLoading(false);
  };

  return (
    <div style={{ marginTop: 25 }}>
      <h2>Rebase</h2>
      <p>Input data for credential issuance</p>
      <p style={{ maxWidth: 500, fontSize: 12 }}>
        You can issue a BasicPostAttestation by filling the fields and clicking the button bellow.
        Title and body can be any string.
      </p>
      <input
        type="text"
        placeholder="Title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        disabled={loading}
      />
      <br />
      <input
        type="text"
        placeholder="Body"
        value={body}
        onChange={(e) => setBody(e.target.value)}
        disabled={loading}
      />
      <br />
      <button
        onClick={issue}
        disabled={loading}
        style={{ marginTop: 15 }}
      >
        <span>
          ISSUE AND POST
        </span>
      </button>
      <p><b>My credentials</b></p>
      <table>
        <tbody>
          {credentialList?.map((content, i) => <tr key={i}>
            <td>
              {content}
            </td>
            <td>
              <button
                onClick={() => handleGetContent(content)}
                disabled={loading}
              >
                <span>
                  GET
                </span>
              </button>
            </td>
            <td>
              <button
                onClick={() => handleDeleteContent(content)}
                disabled={loading}
              >
                <span>
                  DELETE
                </span>
              </button>
            </td>
          </tr>)}
        </tbody>
      </table>
      <pre style={{ marginTop: 25 }}>
        {viewingContent}
      </pre>
    </div>
  );
}

export default RebaseCredentialComponent;

Now update the SSXComponent to import the credential component module by adding the following into my-app/components/SSXComponent.tsx:

"use client";
import { SSX } from "@spruceid/ssx";
import { useState } from "react";
import KeplerStorageComponent from "./KeplerStorageComponent";
import RebaseCredentialComponent from "./RebaseCredentialComponent";

const SSXComponent = () => {

  const [ssxProvider, setSSX] = useState<SSX | null>(null);

  const ssxHandler = async () => {
    const ssx = new SSX({
      providers: {
        server: {
          host: "http://localhost:3000/api"
        }
      },
      modules: {
        storage: {
          prefix: 'my-app',
          hosts: ['https://kepler.spruceid.xyz'],
          autoCreateNewOrbit: true
        }
      }
    });
    await ssx.signIn();
    setSSX(ssx);
  };

  const ssxLogoutHandler = async () => {
    ssxProvider?.signOut();
    setSSX(null);
  };

  const address = ssxProvider?.address() || '';

  return (
    <>
      <h2>User Authorization Module</h2>
      <p>Authenticate and Authorize using your ETH keys</p>
      <br></br>
      {
        ssxProvider ?
          <>
            {
              address &&
              <p>
                <b>Ethereum Address:</b> <code>{address}</code>
              </p>
            }
            <br />
            <button onClick={ssxLogoutHandler}>
              <span>
                Sign-Out
              </span>
            </button>
            <br />
            <KeplerStorageComponent ssx={ssxProvider} />
            <br />
            <RebaseCredentialComponent ssx={ssxProvider} />
          </> :
          <button onClick={ssxHandler}>
            <span>
              Sign-In with Ethereum
            </span>
          </button>
      }
    </>
  );
};

export default SSXComponent;

Finally, you can run the app by using:

npm run dev

Last updated