Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: moving files relevant for embed from iam to identity #3227

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/.env-prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
NEXT_PUBLIC_PASSPORT_GOOGLE_CLIENT_ID='344679350321-hre9kpqn36j2ki7e9tgjf370lhp1jnoi.apps.googleusercontent.com'
NEXT_PUBLIC_PASSPORT_IAM_URL='https://alpha.passport-iam.gitcoin.co/api/'
NEXT_PUBLIC_PASSPORT_INFURA_KEY='460f40a260564ac4a4f4b3fffb032dad'
NEXT_PUBLIC_CERAMIC_CLIENT_URL='https://ceramic-gitcoin.hirenodes.io'
NEXT_PUBLIC_PASSPORT_PROCEDURE_URL='https://alpha.passport-iam.gitcoin.co/procedure/'
NEXT_PUBLIC_PASSPORT_TWITTER_CALLBACK='https://passport.gitcoin.co'
NEXT_PUBLIC_PASSPORT_FACEBOOK_APP_ID='1181664372626753'
NEXT_PUBLIC_PASSPORT_IAM_ISSUER_DID='did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC'
NEXT_PUBLIC_DATADOG_APPLICATION_ID='69327c09-e165-4cbe-a301-f29d722aaac3'
NEXT_PUBLIC_DATADOG_CLIENT_TOKEN='pub25d6786acb4a59f5bdcf700a82bda0bd'
NEXT_PUBLIC_DATADOG_ENV='prod'
NEXT_PUBLIC_PASSPORT_MAINNET_RPC_URL='https://eth-mainnet.g.alchemy.com/v2/nfNUePTbxoK7Xely91TWOrw5kwF3RapF'
NEXT_PUBLIC_PASSPORT_GITHUB_CLIENT_ID='787f1bfb4c88d5cadd54'
NEXT_PUBLIC_PASSPORT_GITHUB_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_PASSPORT_LINKEDIN_CLIENT_ID='781ffdhu6yxgbk'
NEXT_PUBLIC_PASSPORT_LINKEDIN_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_PASSPORT_DISCORD_CLIENT_ID='998739052767494208'
NEXT_PUBLIC_PASSPORT_DISCORD_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_PASSPORT_SIGNER_URL='https://passport-signer.gitcoin.co/'
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID='GTM-WVSZFH6'
NEXT_PUBLIC_PASSPORT_GOOGLE_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_FF_MULTICHAIN_SIGNATURE='on'
NEXT_PUBLIC_FF_MULTI_EVM_SIGNER='on'
NEXT_PUBLIC_CERAMIC_CACHE_ENDPOINT='https://api.scorer.gitcoin.co/ceramic-cache'
NEXT_PUBLIC_PASSPORT_COINBASE_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_PASSPORT_COINBASE_CLIENT_ID='10b9b511a5454e8d30ecf11eb7073053c86d625f676964b0eabfe0cbcb12c74c'
NEXT_PUBLIC_INTERCOM_APP_ID='xaafeyri'
NEXT_PUBLIC_FF_ONE_CLICK_VERIFICATION='on'
NEXT_PUBLIC_SCORER_ENDPOINT='https://api.scorer.gitcoin.co'
NEXT_PUBLIC_FF_LIVE_ALLO_SCORE='on'
NEXT_PUBLIC_MAINTENANCE_MODE_ON='["2023-08-10T23:30:00.000Z","2023-08-11T01:30:00.000Z"]'
NEXT_PUBLIC_PASSPORT_IDENA_WEB_APP='https://app.idena.io/'
NEXT_PUBLIC_FF_IDENA_STAMP='on'
NEXT_PUBLIC_PASSPORT_IDENA_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_FF_NEW_GITHUB_STAMPS='on'
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID='9e280a80328f22e4c4a23593c554388d'
NEXT_PUBLIC_PASSPORT_CIVIC_CALLBACK='https://passport.gitcoin.co/'
NEXT_PUBLIC_WEB3_ONBOARD_EXPLORE_URL='https://passport.gitcoin.co/'
NEXT_PUBLIC_FF_GUILD_STAMP='on'
NEXT_PUBLIC_FF_HOLONYM_STAMP='on'
NEXT_PUBLIC_FF_PHI_STAMP='on'
NEXT_PUBLIC_FF_HYPERCERT_STAMP='on'
NEXT_PUBLIC_FF_NEW_TWITTER_STAMPS='on'
NEXT_PUBLIC_FF_TRUSTALABS_STAMPS='on'
NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS='on'
NEXT_PUBLIC_PASSPORT_IAM_STATIC_URL='https://alpha.passport-iam.gitcoin.co/static/'
NEXT_PUBLIC_FF_CHAIN_SYNC='on'
NEXT_PUBLIC_POSSIBLE_ON_CHAIN_PASSPORT_CHAINIDS='["0x1a8","0xe708","0xa"]'
NEXT_PUBLIC_ACTIVE_ON_CHAIN_PASSPORT_CHAINIDS='["0xa"]'
NEXT_PUBLIC_PASSPORT_OP_RPC_URL='https://opt-mainnet.g.alchemy.com/v2/K_UNos6x7QsRgt-fgpQmX5KzVsDPlWlV'
NEXT_PUBLIC_CERAMIC_CACHE_ENDPOINT_V2='https://api.scorer.gitcoin.co/ceramic-cache/v2'

126 changes: 126 additions & 0 deletions embed/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// ---- Testing libraries

import { Response, Request } from "express";
import axios, { type AxiosResponse } from "axios";
import {
AutoVerificationResponseBodyType,
AutoVerificationRequestBodyType,
autoVerificationHandler,
} from "../src/handlers";
import { ParamsDictionary } from "express-serve-static-core";
import { autoVerifyStamps, AutoVerificationFields } from "@gitcoin/passport-identity";
import { VerifiableCredential } from "@gitcoin/passport-types";

const apiKey = process.env.SCORER_API_KEY;

jest.mock("@gitcoin/passport-identity", () => {
const originalModule = jest.requireActual<typeof import("@gitcoin/passport-identity")>("@gitcoin/passport-identity");

return {
// __esModule: true, // Use it when dealing with esModules
...originalModule,
autoVerifyStamps: jest.fn((autoVerificationFields: AutoVerificationFields): Promise<VerifiableCredential[]> => {
return new Promise((resolve, reject) => {
resolve([] as VerifiableCredential[]);
});
}),
};
});

jest.mock("axios", () => {
const originalModule = jest.requireActual<typeof import("axios")>("axios");

return {
// __esModule: true, // Use it when dealing with esModules
...originalModule,
post: jest.fn((autoVerificationFields: AutoVerificationFields): Promise<AxiosResponse> => {
return new Promise((resolve, reject) => {
resolve({
data: { score: {} },
} as AxiosResponse);
});
}),
};
});

beforeEach(() => {
// CLear the spy stats
jest.clearAllMocks();
});

describe("autoVerificationHandler", function () {
it("properly calls autoVerifyStamps and addStampsAndGetScore", async () => {
// as each signature is unique, each request results in unique output
const request = {
body: {
address: "0x0000000000000000000000000000000000000000",
scorerId: "123",
},
};

const response = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};

await autoVerificationHandler(
request as Request<ParamsDictionary, AutoVerificationResponseBodyType, AutoVerificationRequestBodyType>,
response as undefined as Response
);

expect(autoVerifyStamps).toHaveBeenCalledTimes(1);
expect(autoVerifyStamps).toHaveBeenCalledWith({ ...request.body });

expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
`${process.env.SCORER_ENDPOINT}/embed/stamps/0x0000000000000000000000000000000000000000`,
{
stamps: expect.any(Array),
scorer_id: "123",
},
{
headers: {
Authorization: apiKey,
},
}
);
});

it("properly calls autoVerifyStamps and addStampsAndGetScore when credentialIds are provided", async () => {
// as each signature is unique, each request results in unique output
const request = {
body: {
address: "0x0000000000000000000000000000000000000000",
scorerId: "123",
credentialIds: ["provider-1", "provider-2"],
},
};

const response = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};

await autoVerificationHandler(
request as Request<ParamsDictionary, AutoVerificationResponseBodyType, AutoVerificationRequestBodyType>,
response as undefined as Response
);

expect(autoVerifyStamps).toHaveBeenCalledTimes(1);
expect(autoVerifyStamps).toHaveBeenCalledWith({ ...request.body });

expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
`${process.env.SCORER_ENDPOINT}/embed/stamps/0x0000000000000000000000000000000000000000`,
{
stamps: expect.any(Array),
scorer_id: "123",
},
{
headers: {
Authorization: apiKey,
},
}
);
});
});
76 changes: 74 additions & 2 deletions embed/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
// ---- Web3 packages
import { isAddress } from "ethers";
import * as DIDKit from "@spruceid/didkit-wasm-node";

// ---- Types
import { Response, Request } from "express";
import { ParamsDictionary } from "express-serve-static-core";

// All provider exports from platforms
import { handleAxiosError } from "@gitcoin/passport-platforms";
import { autoVerifyStamps, PassportScore } from "@gitcoin/passport-identity";
import { VerifiableCredential } from "@gitcoin/passport-types";
import {
autoVerifyStamps,
PassportScore,
verifyCredential,
hasValidIssuer,
verifyChallengeAndGetAddress,
VerifyDidChallengeBaseError,
helpers,
groupProviderTypesByPlatform,
verifyProvidersAndIssueCredentials,
} from "@gitcoin/passport-identity";
import { VerifiableCredential, VerifyRequestBody } from "@gitcoin/passport-types";

import axios from "axios";

Expand Down Expand Up @@ -117,3 +128,64 @@ export const autoVerificationHandler = async (
return void errorRes(res, message, 500);
}
};

export const verificationHandler = (
req: Request<ParamsDictionary, AutoVerificationResponseBodyType, VerifyRequestBody>,
res: Response
): void => {
const requestBody: VerifyRequestBody = req.body;
// each verify request should be received with a challenge credential detailing a signature contained in the RequestPayload.proofs
const challenge = requestBody.challenge;
// get the payload from the JSON req body
const payload = requestBody.payload;

// Check the challenge and the payload is valid before issuing a credential from a registered provider
return void verifyCredential(DIDKit, challenge)
.then(async (verified): Promise<void> => {
if (verified && hasValidIssuer(challenge.issuer)) {
let address;
try {
address = await verifyChallengeAndGetAddress(requestBody);
} catch (error) {
if (error instanceof VerifyDidChallengeBaseError) {
return void errorRes(res, `Invalid challenge signature: ${error.name}`, 401);
}
throw error;
}

payload.address = address;

// Check signer and type
const isSigner = challenge.credentialSubject.id === `did:pkh:eip155:1:${address}`;
const isType = challenge.credentialSubject.provider === `challenge-${payload.type}`;

if (!isSigner || !isType) {
return void errorRes(
res,
"Invalid challenge '" +
[!isSigner && "signer", !isType && "provider"].filter(Boolean).join("' and '") +
"'",
401
);
}

const types = payload.types.filter((type) => type);
const providersGroupedByPlatforms = groupProviderTypesByPlatform(types);

const credentials = await verifyProvidersAndIssueCredentials(providersGroupedByPlatforms, address, payload);

return void res.json(credentials);
}

// error response
return void errorRes(res, "Unable to verify payload", 401);
})
.catch((error): void => {
if (error instanceof helpers.ApiError) {
return void errorRes(res, error.message, error.code);
}
let message = "Unable to verify payload";
if (error instanceof Error) message += `: ${error.name}`;
return void errorRes(res, message, 500);
});
};
5 changes: 3 additions & 2 deletions embed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RedisReply, RedisStore } from "rate-limit-redis";

// --- Relative imports
import { keyGenerator, apiKeyRateLimit } from "./rate-limiter.js";
import { autoVerificationHandler } from "./handlers.js";
import { autoVerificationHandler, verificationHandler } from "./handlers.js";
import { metadataHandler } from "./metadata.js";
import { redis } from "./redis.js";

Expand Down Expand Up @@ -106,5 +106,6 @@ app.get("/health", (_req, res) => {
res.status(200).send(data);
});

app.post("/embed/verify", autoVerificationHandler);
app.post("/embed/auto-verify", autoVerificationHandler);
app.post("/embed/verify", verificationHandler);
app.get("/embed/stamps/metadata", metadataHandler);
8 changes: 7 additions & 1 deletion iam/__mocks__/@gitcoin/passport-identity/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ identity.issueChallengeCredential = jest.fn(async (DIDKit, key, record) => ({

// always verifies
identity.verifyCredential = jest.fn(async () => true);

identity.verifyProvidersAndIssueCredentials = jest.fn(async () => []);
identity.verifyDidChallenge = jest.fn().mockImplementation(() => "0x0");
identity.verifyChallengeAndGetAddress = jest.fn().mockImplementation(() => {
return "0x0";
});
identity.hasValidIssuer = jest.fn().mockImplementation(realIdentity.hasValidIssuer);

// return full mock
module.exports = {
...realIdentity,
...identity,
getEip712Issuer: realIdentity.getEip712Issuer,
VerifyDidChallengeBaseError: realIdentity.VerifyDidChallengeBaseError,
realIdentity,
};
2 changes: 1 addition & 1 deletion iam/__tests__/easFees.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getEASFeeAmount } from "../src/utils/easFees";
import { getEASFeeAmount } from "../src/utils/easFees.js";
import { parseEther } from "ethers";
import Moralis from "moralis";
import { PassportCache } from "@gitcoin/passport-platforms";
Expand Down
6 changes: 3 additions & 3 deletions iam/__tests__/easPassportSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as easPassportModule from "../src/utils/easPassportSchema";
import * as easStampModule from "../src/utils/easStampSchema";
import passportOnchainInfo from "../../deployments/onchainInfo.json";
import * as easPassportModule from "../src/utils/easPassportSchema.js";
import * as easStampModule from "../src/utils/easStampSchema.js";
import passportOnchainInfo from "../../deployments/onchainInfo.json" assert { type: "json" };

import { VerifiableCredential } from "@gitcoin/passport-types";
import { NO_EXPIRATION, ZERO_BYTES32 } from "@ethereum-attestation-service/eas-sdk";
Expand Down
4 changes: 2 additions & 2 deletions iam/__tests__/easStampSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as easStampModule from "../src/utils/easStampSchema";
import * as easStampModule from "../src/utils/easStampSchema.js";
import { VerifiableCredential } from "@gitcoin/passport-types";
import { NO_EXPIRATION, ZERO_BYTES32 } from "@ethereum-attestation-service/eas-sdk";
import { SchemaEncoder } from "@ethereum-attestation-service/eas-sdk";
import { parseUnits } from "ethers";
import passportOnchainInfo from "../../deployments/onchainInfo.json";
import passportOnchainInfo from "../../deployments/onchainInfo.json" assert { type: "json" };

jest.mock("../src/utils/scorerService", () => ({
fetchPassportScore: jest.fn().mockImplementation(() => {
Expand Down
Loading
Loading