Skip to content

Run a Verifier

Any application that wants to authenticate users based on their Polygon ID Identity off-chain must set up a Verifier. A Verifier is made of a Server and a Client.

The Server generates the ZK Request according to the requirements of the platform. There are two types of authentication:

  • Basic Auth: For example, a platform that issues Credentials must authenticate users by their identifiers before sharing Credentials with them.
  • Query-based Auth: For example, a platform that gives access only to those users that are over 18 years of age.

The second role of the Server is to execute Verification of the proof sent by the Identity Wallet.

The Verifier Client is the point of interaction with the user. In its simplest form, a client needs to embed a QR code that displays the zk request generated by the Server. The verification request can also be delivered to users via Deep Linking. After scanning the zk request, the user will generate a proof based on that request locally on their wallet. This proof is therefore sent back to the Verifier Server that verifies whether the proof is valid.

This tutorial is based on the verification of a Credential of Type KYCAgeCredential with an attribute birthday with a Schema URL https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld.

The prerequisite is that users have the Polygon ID Wallet app installed and self-issued a Credential of type KYC Age Credential Merklized using our Demo Issuer. Further credentials can be issued using the Issuer Node.

In this example, the verifier will set up the query: "Prove that you were born before the 2000/01/01. To set up a different query check out the ZK Query Language section


Note: The executable code for this section can be found here.


Verifier Server Setup

  1. Add the authorization package to your project

    go get github.com/iden3/go-iden3-auth/v2
    
    npm i @iden3/js-iden3-auth
    
  2. Set up a server

    Initiate a server that contains two endpoints:

    • GET /api/sign-in: Returns auth request.
    • POST /api/callback: Receives the callback request from the identity wallet containing the proof and verifies it.
    package main
    
    import (
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "strconv"
        "time"
    
        "github.com/ethereum/go-ethereum/common"
        "github.com/iden3/go-circuits/v2"
        auth "github.com/iden3/go-iden3-auth/v2"
        "github.com/iden3/go-iden3-auth/v2/loaders"
        "github.com/iden3/go-iden3-auth/v2/pubsignals"
        "github.com/iden3/go-iden3-auth/v2/state"
        "github.com/iden3/iden3comm/v2/protocol"
    )
    
    func main() {
        http.HandleFunc("/api/sign-in", GetAuthRequest)
        http.HandleFunc("/api/callback", Callback)
        http.ListenAndServe(":8080", nil)
    }
    
    // Create a map to store the auth requests and their session IDs
    var requestMap = make(map[string]interface{})
    
    const express = require('express');
    const {auth, resolver, protocol} = require('@iden3/js-iden3-auth')
    const getRawBody = require('raw-body')
    
    const app = express();
    const port = 8080;
    
    app.get("/api/sign-in", (req, res) => {
        console.log('get Auth Request');
        GetAuthRequest(req,res);
    });
    
    app.post("/api/callback", (req, res) => {
        console.log('callback');
        Callback(req,res);
    });
    
    app.listen(port, () => {
        console.log('server running on port 8080');
    });
    
    // Create a map to store the auth requests and their session IDs
    const requestMap = new Map();
    
  3. Sign-in endpoint

    This endpoint generates the auth request for the user. Using this endpoint, the developers set up the requirements that users must meet in order to authenticate.

    If created using Polygon ID Platform, the schema URL can be fetched from there and pasted inside your Query

    func GetAuthRequest(w http.ResponseWriter, r *http.Request) {
    
        // Audience is verifier id
        rURL := "NGROK URL"
        sessionID := 1
        CallbackURL := "/api/callback"
        Audience := "did:polygonid:polygon:mumbai:2qDyy1kEo2AYcP3RT4XGea7BtxsY285szg6yP9SPrs"
    
        uri := fmt.Sprintf("%s%s?sessionId=%s", rURL, CallbackURL, strconv.Itoa(sessionID))
    
        // Generate request for basic authentication
        var request protocol.AuthorizationRequestMessage = auth.CreateAuthorizationRequest("test flow", Audience, uri)
    
        request.ID = "7f38a193-0918-4a48-9fac-36adfdb8b542"
        request.ThreadID = "7f38a193-0918-4a48-9fac-36adfdb8b542"
    
        // Add request for a specific proof
        var mtpProofRequest protocol.ZeroKnowledgeProofRequest
        mtpProofRequest.ID = 1
        mtpProofRequest.CircuitID = string(circuits.AtomicQuerySigV2CircuitID)
        mtpProofRequest.Query = map[string]interface{}{
            "allowedIssuers": []string{"*"},
            "credentialSubject": map[string]interface{}{
                "birthday": map[string]interface{}{
                    "$lt": 20000101,
                },
            },
            "context": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld",
            "type":    "KYCAgeCredential",
        }
        request.Body.Scope = append(request.Body.Scope, mtpProofRequest)
    
        // Store auth request in map associated with session ID
        requestMap[strconv.Itoa(sessionID)] = request
    
        // print request
        fmt.Println(request)
    
        msgBytes, _ := json.Marshal(request)
    
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write(msgBytes)
        return
    }
    
    async function GetAuthRequest(req,res) {
    
        // Audience is verifier id
        const hostUrl = "<NGROK_URL>";
        const sessionId = 1;
        const callbackURL = "/api/callback"
        const audience = "did:polygonid:polygon:mumbai:2qDyy1kEo2AYcP3RT4XGea7BtxsY285szg6yP9SPrs"
    
        const uri = `${hostUrl}${callbackURL}?sessionId=${sessionId}`;
    
        // Generate request for basic authentication
        const request = auth.createAuthorizationRequest(
            'test flow',
            audience,
            uri,
        );
    
        request.id = '7f38a193-0918-4a48-9fac-36adfdb8b542';
        request.thid = '7f38a193-0918-4a48-9fac-36adfdb8b542';
    
        // Add request for a specific proof
        const proofRequest = {
            id: 1,
            circuitId: 'credentialAtomicQuerySigV2',
            query: {
              allowedIssuers: ['*'],
              type: 'KYCAgeCredential',
              context: 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld',
              credentialSubject: {
                birthday: {
                  $lt: 20000101,
                },
              },
          },
          };
        const scope = request.body.scope ?? [];
        request.body.scope = [...scope, proofRequest];
    
        // Store auth request in map associated with session ID
        requestMap.set(`${sessionId}`, request);
    
        return res.status(200).set('Content-Type', 'application/json').send(request);
    }
    

    Allowed Issuers

    When we use * in the "allowed issuers" segment (allowedIssuers: ['*']), we mean that we accept any entity that might have provided the credential. Even though this seems to be convenient for testing purposes, it may also be considered risky. Applying due diligence by actually choosing trusted specific issuers should be the best approach. Only in rare cases, a verifier would accept any issuer, so we advise not to use *.

    Note

    The highlighted lines are to be added only if the authentication needs to design a query for a specific proof as in the case of Query-based Auth. When not included, it will perform a Basic Auth.

  4. Callback Endpoint

    The request generated in the previous endpoint already contains the CallBackURL so that the response generated by the wallet will be automatically forwarded to the server callback function. The callback post endpoint receives the proof generated by the identity wallet. The role of the callback endpoint is to execute the Verification on the proof.

    Testnet / Mainnet

    The code samples on this page are using Polygon's Testnet Mumbai, including the smart contract address and the RPC endpoint in the ethURL variable. If you want to use the Mainnet, you need to add a resolver for it.

    Mainnet contract address: 0x624ce98D2d27b20b8f8d521723Df8fC4db71D79D

    DID prefix: polygon:main

    const RPC_URL = 'RPC_URL>';
    const mainContractAddress = "0x624ce98D2d27b20b8f8d521723Df8fC4db71D79D"
    
    const mainStateResolver = new resolver.EthStateResolver(
        RPC_URL,
        mainContractAddress,
    );
    
    const resolvers = {
        ['polygon:mumbai']: ethStateResolver,
        ['polygon:main']: mainStateResolver,
    };
    

    A Verifier can work with multiple networks simultaneously. Even users and issuers can be on different networks. The verifier library can properly resolve the state of the issuer and the user from the different networks.

    Note

    The public verification keys for Iden3 circuits generated after the trusted setup can be found here and must be added to your project inside a folder called keys.

    // Callback works with sign-in callbacks
    func Callback(w http.ResponseWriter, r *http.Request) {
    
        // Get session ID from request
        sessionID := r.URL.Query().Get("sessionId")
    
        // get JWZ token params from the post request
        tokenBytes, _ := io.ReadAll(r.Body)
    
        // Add Polygon Mumbai RPC node endpoint - needed to read on-chain state
        ethURL := "https://polygon-testnet-rpc.allthatnode.com:8545"
    
        // Add IPFS url - needed to load schemas from IPFS 
        ipfsURL := "https://ipfs.io"
    
        // Add identity state contract address
        contractAddress := "0x134B1BE34911E39A8397ec6289782989729807a4"
    
        resolverPrefix := "polygon:mumbai"
    
        // Locate the directory that contains circuit's verification keys
        keyDIR := "../keys"
    
        // fetch authRequest from sessionID
        authRequest := requestMap[sessionID]
    
        // print authRequest
        fmt.Println(authRequest)
    
        // load the verifcation key
        var verificationKeyloader = &loaders.FSKeyLoader{Dir: keyDIR}
        resolver := state.ETHResolver{
            RPCUrl:          ethURL,
            ContractAddress: common.HexToAddress(contractAddress),
        }
    
        resolvers := map[string]pubsignals.StateResolver{
            resolverPrefix: resolver,
        }
    
        // EXECUTE VERIFICATION
        verifier, err := auth.NewVerifier(verificationKeyloader, resolvers, auth.WithIPFSGateway(ipfsURL))
        if err != nil {
            log.Println(err.Error())
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        authResponse, err := verifier.FullVerify(
            r.Context(),
            string(tokenBytes),
            authRequest.(protocol.AuthorizationRequestMessage),
            pubsignals.WithAcceptedStateTransitionDelay(time.Minute*5))
        if err != nil {
            log.Println(err.Error())
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    
        userID := authResponse.From
    
        messageBytes := []byte("User with ID " + userID + " Successfully authenticated")
    
        w.WriteHeader(http.StatusOK)
        w.Header().Set("Content-Type", "application/json")
        w.Write(messageBytes)
    
        return
    }
    
    async function Callback(req,res) {
    
        // Get session ID from request
        const sessionId = req.query.sessionId;
    
        // get JWZ token params from the post request
        const raw = await getRawBody(req);
        const tokenStr = raw.toString().trim();
    
        const ethURL = '<MUMBAI_RPC_URL>';
        const contractAddress = "0x134B1BE34911E39A8397ec6289782989729807a4"
        const keyDIR = "../keys"
    
        const ethStateResolver = new resolver.EthStateResolver(
            ethURL,
            contractAddress,
          );
    
        const resolvers = {
            ['polygon:mumbai']: ethStateResolver,
        };
    
    
        // fetch authRequest from sessionID
        const authRequest = requestMap.get(`${sessionId}`);
    
    
        // EXECUTE VERIFICATION
        const verifier = await auth.Verifier.newVerifier(
                {
                stateResolver: resolvers,
                circuitsDir: path.join(__dirname, './circuits-dir'),
                ipfsGatewayURL:"<gateway url>"
                }
        );
    
    
    try {
        const opts = {
            AcceptedStateTransitionDelay: 5 * 60 * 1000, // 5 minute
          };        
        authResponse = await verifier.fullVerify(tokenStr, authRequest, opts);
    } catch (error) {
    return res.status(500).send(error);
    }
    return res.status(200).set('Content-Type', 'application/json').send("user with ID: " + authResponse.from + " Succesfully authenticated");
    }
    

If you need to deploy an App or to build a Docker container, you'll need to bundle the libwasmer.so library together with the app.

Verifier Client Setup

The Verifier Client must fetch the Auth Request generated by the Server ("/api/sign-in" endpoint) and deliver it to the user via a QR Code.

To display the QR code inside your frontend, you can use this Code Sandbox

Verifier can show QR that contains one of the following data structures:

  • Raw JSON - message will be threated as on of the IDEN3 Protocol messages.
  • Link with base64 encoded message or shortened request uri (encoded url) in case base64 encoded message is too large. Possible formats of links are:
    1. iden3comm://?i_m={{base64EncodedRequestHere}}
    2. iden3comm://?request_uri={{shortenedUrl}}

if both params are present i_m is priority param and request_uri is ignored.

The same request can also be delivered to users via Deep Linking. Same format for links must be used.

Polygon ID wallet

Polygon ID wallet will support handling of request_uri in the next release, while your client can already implement this specification.

Shortened url algorithm

While it's not strictly restricted how you can perform url shortage algorithm, it is recommended to follow next instruction:

  1. Generate uuid for particular request (or use id of the message itself )
  2. Implement endpoint to fetch message by uuid.
  3. Encode url to fetch message to the request_uri.

Example of url shortage logic:

    package handlers

    import (
        "encoding/json"
        "fmt"
        "io"
        "net/http"
        "os"
        "time"
        "github.com/gofrs/uuid"
        "github.com/patrickmn/go-cache"
    )

    var cacheStorage = cache.New(60*time.Minute, 60*time.Minute)    

    func HandleQRData(w http.ResponseWriter, r *http.Request) {
        switch r.Method {

        // create url for the message
        case http.MethodPost:

            // get json data from request body
            var data interface{}
            body, err := io.ReadAll(r.Body)
            if err != nil {
                http.Error(w, "Failed to read request body", http.StatusInternalServerError)
                return
            }
            defer r.Body.Close()

            err = json.Unmarshal(body, &data)
            if err != nil {
                http.Error(w, "Failed to unmarshal body data", http.StatusInternalServerError)
                return
            }

            // generate random key
            uv, err := uuid.NewV4()

            if err != nil {
                http.Error(w, "Failed to generate uuid", http.StatusInternalServerError)
                return
            }

            // store data in map
            cacheStorage.Set(uv.String(), data, 1*time.Hour)

            hostURL := os.Getenv("HOST_URL") // e.g. https://verifier.com
            // write key to response
            fmt.Fprintf(w, "%s%s?id=%s", hostURL, "api/qr-store", uv.String())
            return  

        // get message by identifier    
        case http.MethodGet:

            // get path param
            id := r.URL.Query().Get("id")
            if id == "" {
                http.Error(w, "Failed to get id", http.StatusNotFound)
                return
            }
            // get data from map
            data, ok := cacheStorage.Get(id)

            if !ok {
                http.Error(w, fmt.Sprintf("Failed to retrieve QR data by %s", id), http.StatusNotFound)
                return
            }

            jsonData, err := json.Marshal(data)
            if err != nil {
                http.Error(w, "Failed to encode JSON", http.StatusInternalServerError)
                return
            }

            // write data to response
            w.WriteHeader(http.StatusOK)
            w.Header().Set("Content-Type", "application/json")
            w.Write(jsonData)
            return
        }
    }
    const express = require('express');
    const { v4: uuidv4 } = require('uuid');
    const Cache = require('cache-manager');
    const HttpStatus = require('http-status-codes');

    const app = express();
    app.use(express.json());

    const cPromise = Cache.caching('memory', {
        max: 100,
        ttl: 10 * 1000 /*milliseconds*/,
    });
    app.get('/api/qr-store', async (req, res) => {
    const id = req.query.id;
    const cacheManager = await cPromise;
    const data = await cacheManager.get(id);

    if (!data) {
        return res.status(HttpStatus.NOT_FOUND).json({ error: `item not found ${id}` });
    }

    return res.status(HttpStatus.OK).json(data);
    });

    app.post('/api/qr-store', async (req, res) => {
    const body = req.body;
    const uuid = uuidv4();
    const cacheManager = await cPromise;

    console.log(cacheManager);

    await cacheManager.set(uuid, body, { ttl: 3600 });

    const hostUrl = process.env.HOST_URL;
    const qrUrl = `${hostUrl}/api/qr-store?id=${uuid}`;

    return res.status(HttpStatus.OK).json({ qrUrl });
    });

    app.listen(3000, () => {
    console.log('Express server is running on port 3000');

    });

Implement Further Logic

This tutorial showcased a minimalistic application that leverages Polygon ID libraries for authentication purposes. Developers can leverage the broad set of existing Credentials held by users to set up any customized Query using our zk Query Language to unleash the full potential of the framework.

For example, the concept can be extended to exchanges that require KYC Credentials, DAOs that require proof-of-personhood Credentials, or social media applications that intend to re-use users' aggregated reputation.

To do so, add the Static Folder to your Verifier repository. This folder contains an HTML static webpage that renders a static webpage with the QR code containing the Auth Request.

To display the QR code inside your frontend, you can use the express.static built-in middleware function together with this Static Folder or this Code Sandbox.

  1. Add routing to your Express Server

    To serve static files, we use the express.static built-in middleware function.

    const express = require('express');
    const {auth, resolver, protocol} = require('@iden3/js-iden3-auth')
    const getRawBody = require('raw-body')
    
    const app = express();
    const port = 8080;
    
    app.use(express.static('static'));
    
    app.get("/api/sign-in", (req, res) => {
        console.log('get Auth Request');
        GetAuthRequest(req,res);
    });
    
    app.post("/api/callback", (req, res) => {
        console.log('callback');
        Callback(req,res);
    });
    
    app.listen(port, () => {
        console.log('server running on port 8080');
    });
    
    // Create a map to store the auth requests and their session IDs
    const requestMap = new Map();
    
  2. Visit http://localhost:8080/

    When visiting the URL, the users will need to scan the QR code with their id wallets.


    Sign Up with Polygon ID - Client Side