UNPKG

@chainwayxyz/phase2cli

Version:

All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies

186 lines (172 loc) 7.58 kB
import open from "open" import figlet from "figlet" import clipboard from "clipboardy" import fetch from "node-fetch" import { getAuth, signInWithCustomToken } from "firebase/auth" import { httpsCallable } from "firebase/functions" import { commonTerms } from "@p0tion/actions" import { showError } from "../lib/errors.js" import { bootstrapCommandExecutionAndServices } from "../lib/services.js" import theme from "../lib/theme.js" import { customSpinner, sleep } from "../lib/utils.js" import { CheckNonceOfSIWEAddressResponse, OAuthDeviceCodeResponse, OAuthTokenResponse } from "../types/index.js" import { checkLocalAccessToken, deleteLocalAccessToken, deleteLocalAuthMethod, getLocalAccessToken, setLocalAccessToken, setLocalAuthMethod } from "../lib/localConfigs.js" const showVerificationCodeAndUri = async (OAuthDeviceCode: OAuthDeviceCodeResponse) => { // Copy code to clipboard. let noClipboard = false try { clipboard.writeSync(OAuthDeviceCode.user_code) clipboard.readSync() } catch (error) { noClipboard = true } // Display data. console.log( `${theme.symbols.warning} Visit ${theme.text.bold( theme.text.underlined(OAuthDeviceCode.verification_uri_complete) )} on this device to generate a new token and authenticate\n` ) console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n") const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : `` console.log( `${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${ theme.symbols.success }\n` ) const spinner = customSpinner(`Redirecting to Sign In With Ethereum...`, `clock`) spinner.start() await sleep(10000) // ~10s to make users able to read the CLI. try { // Automatically open the page (# Step 2). await open(OAuthDeviceCode.verification_uri_complete) } catch (error: any) { console.log( `${theme.symbols.info} Please authenticate via SIWE at ${OAuthDeviceCode.verification_uri_complete}` ) } spinner.stop() } /** * Return the token to sign in to Firebase after passing the SIWE Device Flow * @param clientId <string> - The client id of the Auth0 application. * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function * @returns <string> - The token to sign in to Firebase */ const executeSIWEDeviceFlow = async (clientId: string, firebaseFunctions: any): Promise<string> => { // Call Auth0 endpoint to request device code uri const OAuthDeviceCode = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ client_id: clientId, scope: "openid", audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/` }) }).then((_res) => _res.json())) as OAuthDeviceCodeResponse if (OAuthDeviceCode.error) { showError(OAuthDeviceCode.error_description, true) deleteLocalAuthMethod() deleteLocalAccessToken() } await showVerificationCodeAndUri(OAuthDeviceCode) // Poll Auth0 endpoint until you get token or request expires let isSignedIn = false let isExpired = false let auth0Token = "" while (!isSignedIn && !isExpired) { // Call Auth0 endpoint to request token const OAuthToken = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ client_id: clientId, device_code: OAuthDeviceCode.device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }) }).then((_res) => _res.json())) as OAuthTokenResponse if (OAuthToken.error) { if (OAuthToken.error === "authorization_pending") { // Wait for the user to sign in await sleep(OAuthDeviceCode.interval * 1000) } else if (OAuthToken.error === "slow_down") { // Wait for the user to sign in await sleep(OAuthDeviceCode.interval * 1000 * 2) } else if (OAuthToken.error === "expired_token") { // The user didn't sign in on time isExpired = true } } else { // The user signed in isSignedIn = true auth0Token = OAuthToken.access_token } } // Send token to cloud function to check nonce, create user and retrieve token const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress) const result = await cf({ auth0Token }) const { token, valid, message } = result.data as CheckNonceOfSIWEAddressResponse if (!valid) { showError(message, true) deleteLocalAuthMethod() deleteLocalAccessToken() } return token } /** * Auth command using Sign In With Ethereum * @notice The auth command allows a user to make the association of their Ethereum account with the CLI by leveraging SIWE as an authentication mechanism. * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation. */ const authSIWE = async () => { try { const { firebaseFunctions } = await bootstrapCommandExecutionAndServices() // Console more context for the user. console.log( `${theme.symbols.info} ${theme.text.bold( `You are about to authenticate on this CLI using your Ethereum address (device flow - OAuth 2.0 mechanism).\n${theme.symbols.warning} Please, note that only a Sign-in With Ethereum signature will be required` )}\n` ) const spinner = customSpinner(`Checking authentication token...`, `clock`) spinner.start() await sleep(5000) // Manage OAuth Github or SIWE token. const isLocalTokenStored = checkLocalAccessToken() if (!isLocalTokenStored) { spinner.fail(`No local authentication token found\n`) // Generate a new access token using Github Device Flow (OAuth 2.0). const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions) // Store the new access token. setLocalAuthMethod("siwe") setLocalAccessToken(newToken) } else spinner.succeed(`Local authentication token found\n`) // Get access token from local store. const token = String(getLocalAccessToken()) spinner.text = `Authenticating...` spinner.start() // Exchange token for credential. const credentials = await signInWithCustomToken(getAuth(), token) spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`) console.log( `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold( `phase2cli logout` )} command` ) process.exit(0) } catch (error) { // Delete local token. console.log("An error crashed the process. Deleting local token and identity.") console.error(error) deleteLocalAuthMethod() deleteLocalAccessToken() } } export default authSIWE