@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
text/typescript
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