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

Integrate Outdid stamp in Passport #2426

Open
wants to merge 4 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
1 change: 1 addition & 0 deletions app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ NEXT_PUBLIC_PASSPORT_COINBASE_CALLBACK=http://localhost:3000/
NEXT_PUBLIC_PASSPORT_IDENA_CALLBACK=http://localhost:3000/
NEXT_PUBLIC_PASSPORT_IDENA_WEB_APP=https://app.idena.io/
NEXT_PUBLIC_PASSPORT_CIVIC_CALLBACK=http://localhost:3000/
NEXT_PUBLIC_PASSPORT_OUTDID_CALLBACK=http://localhost:3000/

NEXT_PUBLIC_PASSPORT_SIGNER_URL=http://localhost:8000/

Expand Down
4 changes: 4 additions & 0 deletions app/__test-fixtures__/contextTestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const makeTestCeramicContext = (initialState?: Partial<CeramicContextStat
providerSpec: getProviderSpec("Brightid", "Brightid"),
stamp: undefined,
},
Outdid: {
providerSpec: getProviderSpec("Outdid", "Outdid"),
stamp: undefined,
},
Linkedin: {
providerSpec: getProviderSpec("Linkedin", "Linkedin"),
stamp: undefined,
Expand Down
9 changes: 9 additions & 0 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const {
Idena,
Civic,
TrustaLabs,
Outdid,
} = stampPlatforms;
import { PlatformProps } from "../components/GenericPlatform";

Expand Down Expand Up @@ -172,6 +173,14 @@ platforms.set("Coinbase", {
platFormGroupSpec: Coinbase.ProviderConfig,
});

platforms.set("Outdid", {
platform: new Outdid.OutdidPlatform({
clientId: process.env.NEXT_PUBLIC_OUTDID_API_KEY,
redirectUri: process.env.NEXT_PUBLIC_PASSPORT_OUTDID_CALLBACK,
}),
platFormGroupSpec: Outdid.ProviderConfig,
});

if (process.env.NEXT_PUBLIC_FF_GUILD_STAMP === "on") {
platforms.set("GuildXYZ", {
platform: new GuildXYZ.GuildXYZPlatform(),
Expand Down
1 change: 1 addition & 0 deletions app/public/assets/outdidStampIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions iam/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ COINBASE_CLIENT_ID=MY_COINBASE_CLIENT_ID
COINBASE_CLIENT_SECRET=MY_COINBASE_CLIENT_SECRET
COINBASE_CALLBACK=http://localhost:3000/
TRUSTA_LABS_ACCESS_TOKEN=trusta_labs_access_token
OUTDID_API_KEY=<api key>
OUTDID_API_SECRET=<secret>

CURRENT_ENV=development
EXIT_ON_UNHANDLED_ERROR=true
Expand Down
50 changes: 50 additions & 0 deletions platforms/src/Outdid/App-Bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import axios from "axios";
import { AppContext, PlatformOptions, ProviderPayload } from "../types";
import { Platform } from "../utils/platform";

export class OutdidPlatform extends Platform {
platformId = "Outdid";
path = "outdid";
clientId: string = null;
redirectUri: string = null;

banner = {
heading: "Outdid is an app which scans the NFC chip of your passport and generates a Zero-Knowledge Proof that you are a unique human. Most importantly, all of your private data stays on your phone - not even Outdid can see it :)"
};

constructor(options: PlatformOptions = {}) {
super();
this.clientId = options.clientId as string;
this.redirectUri = options.redirectUri as string;
}

async getProviderPayload(appContext: AppContext): Promise<ProviderPayload> {
const { successRedirect, verificationID } = (await axios.post(`${process.env.NEXT_PUBLIC_PASSPORT_PROCEDURE_URL?.replace(
/\/*?$/,
""
)}/outdid/connect`, {
callback: `${this.redirectUri}?error=false&code=null&state=outdid`,
userDid: appContext.userDid,
})).data as { successRedirect: string, verificationID: string };
const width = 800;
const height = 900;
const left = appContext.screen.width / 2 - width / 2;
const top = appContext.screen.height / 2 - height / 2;

// Pass data to the page via props
appContext.window.open(
successRedirect,
"_blank",
`toolbar=no, location=no, directories=no, status=no, menubar=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${top}, left=${left}`
);

const response = await appContext.waitForRedirect(this);

return {
verificationID,
code: "success",
sessionKey: response.state,
userDid: appContext.userDid,
};
}
}
26 changes: 26 additions & 0 deletions platforms/src/Outdid/Providers-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PlatformSpec, PlatformGroupSpec, Provider } from "../types";
import { OutdidProvider } from "./Providers/outdid";

export const PlatformDetails: PlatformSpec = {
icon: "./assets/outdidStampIcon.svg",
platform: "Outdid",
name: "Outdid",
description: "Outdid's free ZK ID verification brings a strong sybil signal with complete privacy and anonymity.",
connectMessage: "Connect Account",
website: "https://outdid.io/",
};

export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "Name of the Stamp platform group",
providers: [
{
title: "ZK-prove your identity with Outdid",
description: "Outdid uses zero-knowledge cryptography to ensure you are a unique human without revealing any personal information.",
name: "Outdid",
},
]
},
];

export const providers: Provider[] = [new OutdidProvider()]
54 changes: 54 additions & 0 deletions platforms/src/Outdid/Providers/outdid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type Provider, type ProviderOptions } from "../../types";
import type { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";
import axios from "axios";

type OutdidVerification = {
uniqueID: string,
verificationName: string,
status: string,
parameters: { uniqueness: boolean },
}

export class OutdidProvider implements Provider {
// The type will be determined dynamically, from the options passed in to the constructor
type = "Outdid";

// Options can be set here and/or via the constructor
_options: ProviderOptions = {};

// construct the provider instance with supplied options
constructor(options: ProviderOptions = {}) {
this._options = { ...this._options, ...options };
}

async verify(payload: RequestPayload): Promise<VerifiedPayload> {
let valid = false;
let id = "";
const errors: string[] = [];
try {
const verificationID = payload.proofs?.verificationID;

const verificationData: OutdidVerification = await axios.get(`https://api.outdid.io/v1/verification-request?verificationID=${verificationID}`)
.then((response: {data: OutdidVerification}) => response.data);

id = verificationData.uniqueID;
const did = verificationData.verificationName;
if (did === undefined || did === "" || did !== payload.proofs?.userDid) {
errors.push("User verification with Outdid failed.");
} else if (id === undefined || id === "" || JSON.stringify(verificationData.parameters) !== JSON.stringify({ uniqueness: true })) {
errors.push("User could not be verified");
} else {
valid = (verificationData.status === "succeeded");
}
} catch (e) {
errors.push("Error verifying identity with Outdid: " + String(e));
valid = false;
}

return {
valid,
errors,
record: { id },
};
}
}
162 changes: 162 additions & 0 deletions platforms/src/Outdid/__tests__/outdid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/* eslint-disable */
// ---- Test subject
import { RequestPayload } from "@gitcoin/passport-types";
import { OutdidProvider } from "../Providers/outdid";
import { outdidRequestVerification } from "../procedures/outdidVerification";

// ----- Libs
import axios from "axios";

jest.mock("axios");

const mockedAxios = axios as jest.Mocked<typeof axios>;

const userDid = "mock user DID";
const userID = "mock unique user ID";
const verificationID = "11112222";
const redirect = process.env.NEXT_PUBLIC_PASSPORT_OUTDID_CALLBACK;

beforeEach(() => {
jest.clearAllMocks();
});

describe("Attempt Outdid verification", function () {
async function mockProvider(mockedRequest: object) {
mockedAxios.get.mockImplementation(async (url, config) => {
return mockedRequest;
});

const outdid = new OutdidProvider();
const outdidPayload = await outdid.verify({
proofs: {
verificationID,
userDid,
},
} as unknown as RequestPayload);

expect(mockedAxios.get).toBeCalledTimes(1);
expect(mockedAxios.get).toBeCalledWith(
`https://api.outdid.io/v1/verification-request?verificationID=${verificationID}`,
);

return outdidPayload;
}

it("handles valid verification attempt", async () => {
const outdidPayload = await mockProvider({
data: {
verificationName: userDid,
uniqueID: userID,
parameters: { uniqueness: true },
status: "succeeded",
},
status: 200
});
expect(outdidPayload).toEqual({
valid: true,
errors: [],
record: {
id: userID,
},
});
});

it("handles expired verification attempt", async () => {
const outdidPayload = await mockProvider({
data: {
verificationName: userDid,
uniqueID: userID,
parameters: { uniqueness: true },
status: "expired",
},
status: 200
});
expect(outdidPayload).toEqual({
valid: false,
errors: [],
record: {
id: userID,
},
});
});

it("handles unexpected response from Outdid", async () => {
const outdidPayload = await mockProvider({
status: 500
});

expect(outdidPayload).toMatchObject({ valid: false });
});

it("result should not be valid in case user DID is not returned from Outdid", async () => {
const outdidPayload = await mockProvider({
data: {
verificationName: undefined,
uniqueID: userID,
parameters: { uniqueness: true },
status: "succeeded",
},
status: 200
});
expect(outdidPayload).toMatchObject({ valid: false });
});


it("result should not be valid in case no parameters are returned from Outdid", async () => {
const outdidPayload = await mockProvider({
data: {
verificationName: userDid,
parameters: undefined,
status: "failed",
},
status: 200
});
expect(outdidPayload).toMatchObject({ valid: false });
});

it("result should not be valid in case no user identifier is returned from Outdid", async () => {
const outdidPayload = await mockProvider({
data: {
verificationName: userDid,
uniqueID: undefined,
parameters: { uniqueness: true },
status: "succeeded",
},
status: 200
});
expect(outdidPayload).toMatchObject({ valid: false });
});
});


describe("Attempt Outdid request verification", function () {
it("handles valid verification", async () => {
const successRedirect = "http://example.com";
mockedAxios.post.mockImplementation(async (url, _, config) => {
return {
data: {
successRedirect,
verificationID,
},
status: 200,
};
});
const verificationRequestData = await outdidRequestVerification(userDid, redirect);

expect(mockedAxios.post).toBeCalledTimes(1);
expect(mockedAxios.post).toBeCalledWith(
`https://api.outdid.io/v1/verification-request?apiKey=${process.env.OUTDID_API_KEY}&apiSecret=${process.env.OUTDID_API_SECRET}`,
expect.objectContaining({
verificationParameters: { uniqueness: true },
verificationType: "icao",
verificationName: userDid,
redirect,
}),
);

expect(verificationRequestData).toEqual({
successRedirect,
verificationID
});
});
});
3 changes: 3 additions & 0 deletions platforms/src/Outdid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { OutdidPlatform } from "./App-Bindings";
export { ProviderConfig, PlatformDetails, providers } from "./Providers-config";
export { OutdidProvider } from "./Providers/outdid";
15 changes: 15 additions & 0 deletions platforms/src/Outdid/procedures/outdidVerification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from "axios";

type OutdidVerificationResponse = { successRedirect: string, verificationID: string, userDid?: string };

export const outdidRequestVerification = async (userDid: string, redirect: string): Promise<OutdidVerificationResponse> => {
// request a verification containing a unique user identifier
return await axios.post(`https://api.outdid.io/v1/verification-request?apiKey=${process.env.OUTDID_API_KEY}&apiSecret=${process.env.OUTDID_API_SECRET}`, {
verificationParameters: { uniqueness: true },
verificationType: "icao",
verificationName: userDid,
redirect,
}).then((response: { data: OutdidVerificationResponse }) => {
return response.data;
});
}
2 changes: 2 additions & 0 deletions platforms/src/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as Holonym from "./Holonym";
import * as Idena from "./Idena";
import * as Civic from "./Civic";
import * as TrustaLabs from "./TrustaLabs";
import * as Outdid from "./Outdid";
import { PlatformSpec, PlatformGroupSpec, Provider } from "./types";

type PlatformConfig = {
Expand Down Expand Up @@ -50,6 +51,7 @@ const platforms: Record<string, PlatformConfig> = {
Idena,
Civic,
TrustaLabs,
Outdid,
};

if (process.env.NEXT_PUBLIC_FF_NEW_POAP_STAMPS === "on") {
Expand Down
Loading