-
Notifications
You must be signed in to change notification settings - Fork 461
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
317 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { PlatformOptions } from "../types"; | ||
import { Platform } from "../utils/platform"; | ||
|
||
export class LinkedinV2Platform extends Platform { | ||
platformId = "LinkedinV2"; | ||
path = "linkedin"; | ||
|
||
constructor(options: PlatformOptions = {}) { | ||
console.log("Hello DEBUG LinkedinPlatform"); | ||
super(); | ||
console.log("Hello DEBUG LinkedinPlatform options.clientId", options.clientId); | ||
this.clientId = options.clientId as string; | ||
this.redirectUri = options.redirectUri as string; | ||
this.state = options.state as string; | ||
this.banner = { | ||
cta: { | ||
label: "Learn more", | ||
url: "https://support.passport.xyz/passport-knowledge-base/stamps/how-do-i-add-passport-stamps/guide-to-add-a-linkedin-stamp-to-passport", | ||
}, | ||
} | ||
} | ||
|
||
async getOAuthUrl(state: string): Promise<string> { | ||
console.log("Hello DEBUG getOAuthUrl clientId", this.clientId); | ||
const linkedinUrl = await Promise.resolve( | ||
`https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${this.clientId}&redirect_uri=${this.redirectUri}&state=${state}&scope=profile%email` | ||
); | ||
return linkedinUrl; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { PlatformSpec, PlatformGroupSpec, Provider } from "../types"; | ||
import { LinkedinV2Provider } from "./Providers/linkedin"; | ||
|
||
export const PlatformDetails: PlatformSpec = { | ||
icon: "./assets/linkedinStampIcon.svg", | ||
platform: "LinkedinV2", | ||
name: "Linkedin V2", | ||
description: "Connect your existing Linkedin account to verify.", | ||
connectMessage: "Connect Account to V2", | ||
}; | ||
|
||
export const ProviderConfig: PlatformGroupSpec[] = [ | ||
{ | ||
platformGroup: "Account Name V2", | ||
providers: [{ title: "Encrypted V2", name: "LinkedinV2" }], | ||
}, | ||
]; | ||
|
||
export const providers: Provider[] = [new LinkedinV2Provider()]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// ----- Types | ||
import type { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types"; | ||
import { ProviderExternalVerificationError, type Provider, type ProviderOptions } from "../../types"; | ||
|
||
// ----- Libs | ||
import axios from "axios"; | ||
|
||
// ----- Utils | ||
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError"; | ||
|
||
export type LinkedinTokenResponse = { | ||
access_token: string; | ||
}; | ||
|
||
export type LinkedinFindMyUserResponse = { | ||
id?: string; | ||
firstName?: string; | ||
lastName?: string; | ||
error?: string; | ||
}; | ||
|
||
// Export a Linkedin Provider to carry out OAuth and return a record object | ||
export class LinkedinV2Provider implements Provider { | ||
// Give the provider a type so that we can select it with a payload | ||
type = "LinkedinV2"; | ||
|
||
// Options can be set here and/or via the constructor | ||
_options = {}; | ||
|
||
// construct the provider instance with supplied options | ||
constructor(options: ProviderOptions = {}) { | ||
console.log("Hello DEBUG LinkedinProvider"); | ||
this._options = { ...this._options, ...options }; | ||
} | ||
|
||
// verify that the proof object contains valid === "true" | ||
async verify(payload: RequestPayload): Promise<VerifiedPayload> { | ||
console.log("Hello DEBUG verify"); | ||
const errors = []; | ||
let valid = false, | ||
verifiedPayload: LinkedinFindMyUserResponse = {}, | ||
record = undefined; | ||
|
||
try { | ||
if (payload.proofs) { | ||
verifiedPayload = await verifyLinkedin(payload.proofs.code); | ||
valid = verifiedPayload && verifiedPayload.id ? true : false; | ||
|
||
if (valid) { | ||
record = { | ||
id: verifiedPayload.id, | ||
}; | ||
} else { | ||
errors.push(`We were unable to verify your LinkedIn account -- LinkedIn Account Valid: ${String(valid)}.`); | ||
} | ||
} else { | ||
errors.push(verifiedPayload.error); | ||
} | ||
return { | ||
valid, | ||
record, | ||
errors, | ||
}; | ||
} catch (e: unknown) { | ||
throw new ProviderExternalVerificationError(`LinkedIn Account verification error: ${JSON.stringify(e)}.`); | ||
} | ||
} | ||
} | ||
|
||
const requestAccessToken = async (code: string): Promise<string> => { | ||
try { | ||
console.log("Hello DEBUG requestAccessToken"); | ||
const clientId = process.env.LINKEDIN_CLIENT_ID_V2; | ||
console.log("Hello DEBUG clientId", clientId); | ||
const clientSecret = process.env.LINKEDIN_CLIENT_SECRET_V2; | ||
|
||
const tokenRequest = await axios.post( | ||
`https://www.linkedin.com/oauth/v2/accessToken?grant_type=authorization_code&code=${code}&client_id=${clientId}&client_secret=${clientSecret}&redirect_uri=${process.env.LINKEDIN_CALLBACK}`, | ||
{}, | ||
{ | ||
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, | ||
} | ||
); | ||
|
||
if (tokenRequest.status != 200) { | ||
throw `Post for request returned status code ${tokenRequest.status} instead of the expected 200`; | ||
} | ||
|
||
const tokenResponse = tokenRequest.data as LinkedinTokenResponse; | ||
|
||
return tokenResponse.access_token; | ||
} catch (e: unknown) { | ||
handleProviderAxiosError(e, "LinkedIn access token request"); | ||
return String(e); | ||
} | ||
}; | ||
|
||
const verifyLinkedin = async (code: string): Promise<LinkedinFindMyUserResponse> => { | ||
try { | ||
console.log("Hello DEBUG verifyLinkedin"); | ||
// retrieve user's auth bearer token to authenticate client | ||
const accessToken = await requestAccessToken(code); | ||
// Now that we have an access token fetch the user details | ||
const userRequest = await axios.get("https://api.linkedin.com/v2/userinfo", { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
"Linkedin-Version": 202305, | ||
}, | ||
}); | ||
|
||
return userRequest.data as LinkedinFindMyUserResponse; | ||
} catch (e: unknown) { | ||
handleProviderAxiosError(e, "LinkedIn verification", [code]); | ||
return e; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* eslint-disable @typescript-eslint/require-await */ | ||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
// ---- Test subject | ||
import { LinkedinProvider } from "../../Linkedin/Providers/linkedin"; | ||
|
||
import { RequestPayload } from "@gitcoin/passport-types"; | ||
|
||
// ----- Libs | ||
import axios from "axios"; | ||
|
||
jest.mock("axios"); | ||
|
||
const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
|
||
const validLinkedinUserResponse = { | ||
data: { | ||
id: "18723656", | ||
firstName: "First", | ||
lastName: "Last", | ||
}, | ||
status: 200, | ||
}; | ||
|
||
const validCodeResponse = { | ||
data: { | ||
access_token: "762165719dhiqudgasyuqwt6235", | ||
}, | ||
status: 200, | ||
}; | ||
|
||
const code = "ABC123_ACCESSCODE"; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
mockedAxios.post.mockImplementation(async () => { | ||
return validCodeResponse; | ||
}); | ||
|
||
mockedAxios.get.mockImplementation(async () => { | ||
return validLinkedinUserResponse; | ||
}); | ||
}); | ||
|
||
describe("Attempt verification", function() { | ||
it("handles valid verification attempt", async () => { | ||
const clientId = process.env.LINKEDIN_CLIENT_ID; | ||
const clientSecret = process.env.LINKEDIN_CLIENT_SECRET; | ||
const linkedin = new LinkedinProvider(); | ||
const linkedinPayload = await linkedin.verify({ | ||
proofs: { | ||
code, | ||
}, | ||
} as unknown as RequestPayload); | ||
|
||
// Check the request to get the token | ||
expect(mockedAxios.post).toHaveBeenCalledWith( | ||
`https://www.linkedin.com/oauth/v2/accessToken?grant_type=authorization_code&code=${code}&client_id=${clientId}&client_secret=${clientSecret}&redirect_uri=${process.env.LINKEDIN_CALLBACK}`, | ||
{}, | ||
{ | ||
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, | ||
} | ||
); | ||
|
||
// Check the request to get the user | ||
expect(mockedAxios.get).toHaveBeenCalledWith("https://api.linkedin.com/v2/me", { | ||
headers: { Authorization: "Bearer 762165719dhiqudgasyuqwt6235", "Linkedin-Version": 202305 }, | ||
}); | ||
|
||
expect(linkedinPayload).toEqual({ | ||
valid: true, | ||
errors: [], | ||
record: { | ||
id: validLinkedinUserResponse.data.id, | ||
}, | ||
}); | ||
}); | ||
|
||
it("should return invalid payload when unable to retrieve auth token", async () => { | ||
mockedAxios.post.mockRejectedValueOnce("bad request"); | ||
|
||
const linkedin = new LinkedinProvider(); | ||
|
||
await expect( | ||
async () => | ||
await linkedin.verify({ | ||
proofs: { | ||
code, | ||
}, | ||
} as unknown as RequestPayload) | ||
).rejects.toThrow("LinkedIn Account verification error: "); | ||
}); | ||
|
||
it("should return invalid payload when there is no id in verifyLinkedin response", async () => { | ||
mockedAxios.get.mockImplementation(async () => { | ||
return { | ||
data: { | ||
id: undefined, | ||
firstName: "First", | ||
lastName: "Last", | ||
}, | ||
status: 200, | ||
}; | ||
}); | ||
|
||
const linkedin = new LinkedinProvider(); | ||
|
||
const linkedinPayload = await linkedin.verify({ | ||
proofs: { | ||
code, | ||
}, | ||
} as unknown as RequestPayload); | ||
|
||
expect(linkedinPayload).toMatchObject({ valid: false }); | ||
}); | ||
|
||
it("should return invalid payload when a bad status code is returned by linkedin user api", async () => { | ||
mockedAxios.get.mockRejectedValueOnce(async () => { | ||
return { | ||
status: 500, | ||
}; | ||
}); | ||
|
||
const linkedin = new LinkedinProvider(); | ||
|
||
await expect( | ||
async () => | ||
await linkedin.verify({ | ||
proofs: { | ||
code, | ||
}, | ||
} as unknown as RequestPayload) | ||
).rejects.toThrow("LinkedIn Account verification error: "); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { LinkedinV2Provider } from "./Providers/linkedin"; | ||
export { PlatformDetails, ProviderConfig, providers } from "./Providers-config"; | ||
export { LinkedinV2Platform } from "./App-Bindings"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters