Skip to content

Commit

Permalink
Add passport verification (#28)
Browse files Browse the repository at this point in the history
* working endpoint

* reurn multiple verifications

* fixes

* own verifier

* works now

* rm logs
  • Loading branch information
0xKurt authored Nov 28, 2024
1 parent f44675d commit a9e2ffe
Show file tree
Hide file tree
Showing 7 changed files with 1,990 additions and 117 deletions.
1,712 changes: 1,595 additions & 117 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"ts-node-dev": "^2.0.0"
},
"dependencies": {
"@gitcoin/gs-passport-verifier": "^0.0.1",
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
Expand Down
27 changes: 27 additions & 0 deletions src/controllers/passportValidationController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Request, Response } from 'express';
import { validateRequest } from '@/utils';
import type { ProjectApplicationForManager } from '@/ext/passport/types';
import { isVerified } from '@/ext/passport/credentialverification';

interface SocialCredentialBody {
application: Partial<ProjectApplicationForManager>;
}

export const validateSocialCredential = async (
req: Request,
res: Response
): Promise<void> => {
validateRequest(req, res);

const { application } = req.body as SocialCredentialBody;

try {
const result = await isVerified(application);
res.json({
message: 'Social credential validated',
provider: result,
});
} catch (error) {
res.status(400).json({ message: 'Error validating social credential' });
}
};
94 changes: 94 additions & 0 deletions src/ext/passport/credentialverification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { PassportVerifier } from '@gitcoin/gs-passport-verifier';
import type {
VerifiableCredential,
ProjectApplicationForManager,
ProjectApplicationMetadata,
} from './types';

export const IAM_SERVER =
'did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC';

const verifier = new PassportVerifier();

export async function isVerified(
application: Partial<ProjectApplicationForManager> | undefined
): Promise<{
twitter: { isVerified: boolean };
github: { isVerified: boolean };
}> {
const applicationMetadata = application?.metadata;

const verifyCredential = async (
provider: 'twitter' | 'github'
): Promise<boolean> => {
const verifiableCredential =
applicationMetadata?.application.project.credentials[provider];
if (verifiableCredential === undefined) {
return false;
}

const verifiedCredential =
await verifier.verifyCredentialAndExpiry(verifiableCredential);

const vcHasValidProof = verifiedCredential?.valid;

const vcIssuedByValidIAMServer = verifiableCredential.issuer === IAM_SERVER;
const providerMatchesProject = vcProviderMatchesProject(
provider,
verifiableCredential,
applicationMetadata
);

const roleAddresses = application?.canonicalProject?.roles.map(
role => role.address
);
const vcIssuedToAtLeastOneProjectOwner = (roleAddresses ?? []).some(role =>
vcIssuedToAddress(verifiableCredential, role.toLowerCase())
);

return (
vcHasValidProof &&
vcIssuedByValidIAMServer &&
providerMatchesProject &&
vcIssuedToAtLeastOneProjectOwner
);
};

const [twitterVerified, githubVerified] = await Promise.all([
verifyCredential('twitter'),
verifyCredential('github'),
]);

return {
twitter: { isVerified: twitterVerified },
github: { isVerified: githubVerified },
};
}

function vcIssuedToAddress(vc: VerifiableCredential, address: string): boolean {
const vcIdSplit = vc.credentialSubject.id.split(':');
const addressFromId = vcIdSplit[vcIdSplit.length - 1];
return addressFromId.toLowerCase() === address.toLowerCase();
}

function vcProviderMatchesProject(
provider: string,
verifiableCredential: VerifiableCredential,
applicationMetadata: ProjectApplicationMetadata | undefined
): boolean {
let vcProviderMatchesProject = false;
if (provider === 'twitter') {
vcProviderMatchesProject =
verifiableCredential.credentialSubject.provider
?.split('#')[1]
.toLowerCase() ===
applicationMetadata?.application.project?.projectTwitter?.toLowerCase();
} else if (provider === 'github') {
vcProviderMatchesProject =
verifiableCredential.credentialSubject.provider
?.split('#')[1]
.toLowerCase() ===
applicationMetadata?.application.project?.projectGithub?.toLowerCase();
}
return vcProviderMatchesProject;
}
130 changes: 130 additions & 0 deletions src/ext/passport/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
export interface VerifiableCredential {
'@context': string[];
type: string[];
credentialSubject: {
id: string;
'@context': Array<Record<string, string>>;
hash?: string;
provider?: string;
address?: string;
challenge?: string;
};
issuer: string;
issuanceDate: string;
expirationDate: string;
proof: {
type: string;
proofPurpose: string;
verificationMethod: string;
created: string;
jws: string;
};
}

export type ProjectCredentials = Record<string, VerifiableCredential>;

export interface ProjectOwner {
address: string;
}

export interface ProjectMetadata {
title: string;
description: string;
website: string;
bannerImg?: string;
logoImg?: string;
projectTwitter?: string;
userGithub?: string;
projectGithub?: string;
credentials: ProjectCredentials;
owners: ProjectOwner[];
recipient?: string;
createdAt: number;
lastUpdated: number;
}

export interface ApplicationAnswer {
type: string;
hidden: boolean;
question: string;
questionId: number;
encryptedAnswer?: {
ciphertext: string;
encryptedSymmetricKey: string;
};
answer: string;
}

export interface ProjectApplicationMetadata {
signature: string;
application: {
round: string;
answers: ApplicationAnswer[];
project: ProjectMetadata;
recipient: string;
};
}

export interface BaseDonorValues {
totalAmountDonatedInUsd: number;
totalDonationsCount: number;
uniqueDonorsCount: number;
}

export type ApplicationStatus =
| 'PENDING'
| 'APPROVED'
| 'IN_REVIEW'
| 'REJECTED'
| 'APPEAL'
| 'FRAUD'
| 'RECEIVED'
| 'CANCELLED';

export interface ProjectApplication extends BaseDonorValues {
id: string;
projectId: string;
chainId: number;
roundId: string;
status: ApplicationStatus;
metadataCid: string;
metadata: ProjectApplicationMetadata;
distributionTransaction: string | null;
}

interface StatusSnapshot {
status: ApplicationStatus;
updatedAtBlock: string;
updatedAt: string;
}

export interface ProjectApplicationForManager extends ProjectApplication {
anchorAddress: `0x${string}`;
statusSnapshots: StatusSnapshot[];
round: {
strategyName: string;
strategyAddress: string;
roundMetadata: {
name: string;
};
applicationsStartTime: string;
applicationsEndTime: string;
donationsEndTime: string;
donationsStartTime: string;
};
canonicalProject: {
roles: Array<{ address: `0x${string}` }>;
};
}

export interface PastApplication {
id: string;
roundId: string;
statusSnapshots: StatusSnapshot[];
status: ApplicationStatus;
round: {
roundMetadata: {
name: string;
};
};
}
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Router } from 'express';
import evaluationRoutes from '@/routes/evaluationRoutes';
import poolRoutes from '@/routes/poolRoutes';
import passportRoutes from '@/routes/passportValidationRoutes';

const router = Router();

router.use('/evaluate', evaluationRoutes);
router.use('/pools', poolRoutes);
router.use('/passport', passportRoutes);

export default router;
Loading

0 comments on commit a9e2ffe

Please sign in to comment.