forked from passportxyz/passport
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(github): implementing github stamp
- Loading branch information
Showing
11 changed files
with
296 additions
and
5 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// --- Methods | ||
import React, { useContext, useEffect, useState } from "react"; | ||
import { debounce } from "ts-debounce"; | ||
import { BroadcastChannel } from "broadcast-channel"; | ||
|
||
// --- Identity tools | ||
import { PROVIDER_ID } from "@dpopp/types"; | ||
import { fetchVerifiableCredential } from "@dpopp/identity"; | ||
|
||
// --- Components | ||
import { Card } from "../Card"; | ||
|
||
// --- Context | ||
import { UserContext } from "../../context/userContext"; | ||
import { ProviderSpec } from "../../config/providers"; | ||
import { datadogLogs } from "@datadog/browser-logs"; | ||
|
||
// Each provider is recognised by its ID | ||
const providerId: PROVIDER_ID = "Github"; | ||
|
||
export default function GithubCard(): JSX.Element { | ||
const { address, signer, handleAddStamp, allProvidersState } = useContext(UserContext); | ||
const [isLoading, setLoading] = useState(false); | ||
|
||
// Fetch Github OAuth2 url from the IAM procedure | ||
async function handleFetchGithubOAuth(): Promise<void> { | ||
// Fetch data from external API | ||
const res = await fetch( | ||
`${process.env.NEXT_PUBLIC_DPOPP_PROCEDURE_URL?.replace(/\/*?$/, "")}/github/generateAuthUrl`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
callback: process.env.NEXT_PUBLIC_DPOPP_GITHUB_CALLBACK, | ||
}), | ||
} | ||
); | ||
const data = await res.json(); | ||
// open new window for authUrl | ||
const githubUrl = data.authUrl; | ||
|
||
openGithubOAuthUrl(githubUrl); | ||
} | ||
|
||
// Open Twitter authUrl in centered window | ||
function openGithubOAuthUrl(url: string): void { | ||
const width = 600; | ||
const height = 800; | ||
const left = screen.width / 2 - width / 2; | ||
const top = screen.height / 2 - height / 2; | ||
|
||
// Pass data to the page via props | ||
window.open( | ||
url, | ||
"_blank", | ||
"toolbar=no, location=no, directories=no, status=no, menubar=no, resizable=no, copyhistory=no, width=" + | ||
width + | ||
", height=" + | ||
height + | ||
", top=" + | ||
top + | ||
", left=" + | ||
left | ||
); | ||
} | ||
|
||
// Listener to watch for oauth redirect response on other windows (on the same host) | ||
function listenForRedirect(e: { target: string; data: { code: string; state: string } }) { | ||
// when receiving github oauth response from a spawned child run fetchVerifiableCredential | ||
if (e.target === "github") { | ||
// pull data from message | ||
const queryCode = e.data.code; | ||
const queryState = e.data.state; | ||
|
||
datadogLogs.logger.info("Saving Stamp", { provider: "Github" }); | ||
// fetch and store credential | ||
setLoading(true); | ||
fetchVerifiableCredential( | ||
process.env.NEXT_PUBLIC_DPOPP_IAM_URL || "", | ||
{ | ||
type: providerId, | ||
version: "0.0.0", | ||
address: address || "", | ||
proofs: { | ||
code: queryCode, // provided by github as query params in the redirect | ||
sessionKey: queryState, | ||
}, | ||
}, | ||
signer as { signMessage: (message: string) => Promise<string> } | ||
) | ||
.then(async (verified: { credential: any }): Promise<void> => { | ||
await handleAddStamp({ | ||
provider: providerId, | ||
credential: verified.credential, | ||
}); | ||
datadogLogs.logger.info("Successfully saved Stamp", { provider: "Github" }); | ||
}) | ||
.finally(() => { | ||
setLoading(false); | ||
}); | ||
} | ||
} | ||
|
||
// attach and destroy a BroadcastChannel to handle the message | ||
useEffect(() => { | ||
// open the channel | ||
const channel = new BroadcastChannel("github_oauth_channel"); | ||
// event handler will listen for messages from the child (debounced to avoid multiple submissions) | ||
channel.onmessage = debounce(listenForRedirect, 300); | ||
|
||
return () => { | ||
channel.close(); | ||
}; | ||
}); | ||
|
||
const issueCredentialWidget = ( | ||
<button data-testid="button-verify-github" className="verify-btn" onClick={handleFetchGithubOAuth}> | ||
Connect account | ||
</button> | ||
); | ||
|
||
return ( | ||
<Card | ||
providerSpec={allProvidersState[providerId]!.providerSpec as ProviderSpec} | ||
verifiableCredential={allProvidersState[providerId]!.stamp?.credential} | ||
issueCredentialWidget={issueCredentialWidget} | ||
isLoading={isLoading} | ||
/> | ||
); | ||
} |
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,54 @@ | ||
import crypto from "crypto"; | ||
import axios from "axios"; | ||
|
||
export const getSessionKey = (): string => { | ||
return `github-${crypto.randomBytes(32).toString("hex")}`; | ||
}; | ||
|
||
export type GithubTokenResponse = { | ||
access_token: string; | ||
}; | ||
|
||
export type GithubFindMyUserResponse = { | ||
id?: string; | ||
login?: string; | ||
type?: string; | ||
}; | ||
|
||
const requestAccessToken = async (code: string): Promise<string> => { | ||
const clientId = process.env.GITHUB_CLIENT_ID; | ||
const clientSecret = process.env.GITHUB_CLIENT_SECRET; | ||
|
||
// Exchange the code for an access token | ||
const tokenRequest = await axios.post( | ||
`https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, | ||
{}, | ||
{ | ||
headers: { Accept: "application/json" }, | ||
} | ||
); | ||
|
||
if (tokenRequest.status != 200) { | ||
throw `Post for request returned status code ${tokenRequest.status} instead of the expected 200`; | ||
} | ||
|
||
const tokenResponse = tokenRequest.data as GithubTokenResponse; | ||
|
||
return tokenResponse.access_token; | ||
}; | ||
|
||
export const requestFindMyUser = async (code: string): Promise<GithubFindMyUserResponse> => { | ||
// 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.github.com/user", { | ||
headers: { Authorization: `token ${accessToken}` }, | ||
}); | ||
|
||
if (userRequest.status != 200) { | ||
throw `Get user request returned status code ${userRequest.status} instead of the expected 200`; | ||
} | ||
|
||
return userRequest.data as GithubFindMyUserResponse; | ||
}; |
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,46 @@ | ||
// ----- Types | ||
import type { RequestPayload, VerifiedPayload } from "@dpopp/types"; | ||
|
||
// ----- Github OAuth2 | ||
import { GithubFindMyUserResponse, requestFindMyUser } from "../procedures/githubOauth"; | ||
import type { Provider, ProviderOptions } from "../types"; | ||
|
||
// Export a Github Provider to carry out OAuth and return a record object | ||
export class GithubProvider implements Provider { | ||
// Give the provider a type so that we can select it with a payload | ||
type = "Github"; | ||
|
||
// Options can be set here and/or via the constructor | ||
_options = {}; | ||
|
||
// construct the provider instance with supplied options | ||
constructor(options: ProviderOptions = {}) { | ||
this._options = { ...this._options, ...options }; | ||
} | ||
|
||
// verify that the proof object contains valid === "true" | ||
async verify(payload: RequestPayload): Promise<VerifiedPayload> { | ||
let valid = false, | ||
verifiedPayload: GithubFindMyUserResponse = {}; | ||
|
||
try { | ||
verifiedPayload = await verifyGithub(payload.proofs.code); | ||
} catch (e) { | ||
return { valid: false }; | ||
} finally { | ||
valid = verifiedPayload && verifiedPayload.id ? true : false; | ||
} | ||
|
||
return { | ||
valid: valid, | ||
record: { | ||
id: verifiedPayload.id, | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
// Perform verification on twitter access token | ||
async function verifyGithub(code: string): Promise<GithubFindMyUserResponse> { | ||
return await requestFindMyUser(code); | ||
} |
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