Run a Verifier
Any application that wants to authenticate users based on their Privado 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 Universal Link or a QR code that contains the ZK request generated by the Server so that it reaches the user's wallet. The verification request can also be delivered to users via Deep Linking. Once the ZK request reaches the user's wallet, 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
based on the following Schema URL: https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld
.
The prerequisite is that users have self-issued a Credential of type KYC Age Credential Merklized
using our Demo Issuer on their Web Wallet or Privado Wallet App. 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 2000/01/01
. To set up a different query check out the ZK Query Language section.
The executable code for this section can be found here.
Verifier Server Setup
- Add the authorization package to your project
- Golang
- Javascript
go get github.com/iden3/go-iden3-auth/v2
npm i @iden3/js-iden3-auth
- 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.
- Golang
- Javascript
package main
import(
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/ethereum/go-ethereum/common"
circuits "github.com/iden3/go-circuits/v2"
auth "github.com/iden3/go-iden3-auth/v2"
"github.com/iden3/go-iden3-auth/v2/pubsignals"
"github.com/iden3/go-iden3-auth/v2/state"
"github.com/iden3/iden3comm/v2/protocol"
)
const VerificationKeyPath = "verification_key.json"
type KeyLoader struct {
Dir string
}
// Load keys from embedded FS
func (m KeyLoader) Load(id circuits.CircuitID) ([]byte, error) {
return os.ReadFile(fmt.Sprintf("%s/%v/%s", m.Dir, id, VerificationKeyPath))
}
func main() {
http.HandleFunc("/api/sign-in", GetAuthRequest)
http.HandleFunc("/api/callback", Callback)
log.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
// 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();
- 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.
- Golang
- Javascript
func GetAuthRequest(w http.ResponseWriter, r *http.Request) {
// Audience is verifier id
rURL := "NGROK URL"
sessionID := 1
CallbackURL := "/api/callback"
Audience := "did:polygonid:polygon:amoy:2qQ68JkRcf3xrHPQPWZei3YeVzHPP58wYNxx2mEouR"
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:amoy:2qQ68JkRcf3xrHPQPWZei3YeVzHPP58wYNxx2mEouR";
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);
}
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 *
.
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.
- 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.
The code samples on this page utilize Polygon's Amoy testnet, which is associated with the verifier, and the Privado Identity Chain, which is associated with the users' identities. This includes the smart contract addresses and RPC endpoints specified in the resolvers
object. If you wish to use a different network, you will need to add a resolver for it.
You can find the addresses for the validator smart contracts here. Below is an example for adding a resolver for polygon mainnet:
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: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.
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
.
- Golang
- Javascript
// Callback works with sign-in callbacks
func Callback(w http.ResponseWriter, r *http.Request) {
fmt.Println("callback")
// Get session ID from request
sessionID := r.URL.Query().Get("sessionId")
// get JWZ token params from the post request
tokenBytes, err := io.ReadAll(r.Body)
if err != nil {
log.Println(err)
return
}
// Add IPFS url - needed to load schemas from IPFS
ipfsURL := "https://ipfs.io"
// Locate the directory that contains circuit's verification keys
keyDIR := "../keys"
// fetch authRequest from sessionID
authRequest := requestMap[sessionID]
// print authRequest
log.Println(authRequest)
// load the verifcation key
var verificationKeyLoader = &KeyLoader{Dir: keyDIR}
resolver := state.ETHResolver{
"polygon:amoy": {
RPCUrl: "<AMOY_RPC_URL>",
ContractAddress: common.HexToAddress("0x1a4cC30f2aA0377b0c3bc9848766D90cb4404124"),
},
"privado:main": {
RPCUrl: "https://rpc-mainnet.privado.id",
ContractAddress: common.HexToAddress("0x3C9acB2205Aa72A05F6D77d708b5Cf85FCa3a896"),
}
}
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
}
//marshal auth resp
messageBytes, err := json.Marshal(authResponse)
if err != nil {
log.Println(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(messageBytes)
log.Println("verification passed")
}
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();
console.log(tokenStr);
const keyDIR = "../keys";
const resolvers = {
["polygon:amoy"]: new resolver.EthStateResolver(
"<AMOY_RPC_URL>",
"0x1a4cC30f2aA0377b0c3bc9848766D90cb4404124"
),
["privado:main"]: new resolver.EthStateResolver(
"https://rpc-mainnet.privado.id",
"0x3C9acB2205Aa72A05F6D77d708b5Cf85FCa3a896"
)
};
// 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(authResponse);
}
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 the Universal Link or QR Code.
Please Refer this to know how to configure Universal Links.
Universal Links can be used to support both the Web Wallet and the mobile wallet app, while QR codes or deep links are limited to use with mobile devices.
QR Code setup (only supported on mobile):
A Verifier can show a QR code that contains one of the following data structures:
- Raw JSON - message will be treated as one 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:
iden3comm://?i_m={{base64EncodedRequestHere}}
iden3comm://?request_uri={{shortenedUrl}}
If both params are present i_m
is prioritized and request_uri
is ignored.
The same request can also be delivered to users via Deep Linking. The same format for links must be used.
To display the QR code inside your frontend, you can use this Code Sandbox.
Implement Further Logic
This tutorial showcased a minimalistic application that leverages Privado 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.
Step 1: Include the Static Folder
Add the Static Folder to your Verifier repository.
This folder contains:
- index.html: The main HTML page that renders the QR code and button.
- styles.css: The CSS file for styling.
- script.js: The JavaScript file that fetches the API data and generates the button containing the Universal Link and a QR code.
Step 2: Serve Static Files Using Express
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();
Step 3: Visit Your Application
Start your server and visit http://localhost:8080/. When visiting the URL, users can click the button containing the Universal Link or scan the QR code with their Privado ID wallet app and continue the verification process.