123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.logout = exports.getAccessToken = exports.findAccountByEmail = exports.loginGithub = exports.loginGoogle = exports.setGlobalDefaultAccount = exports.setProjectAccount = exports.loginAdditionalAccount = exports.selectAccount = exports.setRefreshToken = exports.setActiveAccount = exports.getAllAccounts = exports.getAdditionalAccounts = exports.getProjectDefaultAccount = exports.getGlobalDefaultAccount = void 0;
- const clc = require("colorette");
- const FormData = require("form-data");
- const fs = require("fs");
- const http = require("http");
- const jwt = require("jsonwebtoken");
- const opn = require("open");
- const path = require("path");
- const portfinder = require("portfinder");
- const url = require("url");
- const util = require("util");
- const apiv2 = require("./apiv2");
- const configstore_1 = require("./configstore");
- const error_1 = require("./error");
- const utils = require("./utils");
- const logger_1 = require("./logger");
- const prompt_1 = require("./prompt");
- const scopes = require("./scopes");
- const defaultCredentials_1 = require("./defaultCredentials");
- const uuid_1 = require("uuid");
- const crypto_1 = require("crypto");
- const track_1 = require("./track");
- const api_1 = require("./api");
- portfinder.setBasePort(9005);
- function getGlobalDefaultAccount() {
- const user = configstore_1.configstore.get("user");
- const tokens = configstore_1.configstore.get("tokens");
- if (!user || !tokens) {
- return undefined;
- }
- return {
- user,
- tokens,
- };
- }
- exports.getGlobalDefaultAccount = getGlobalDefaultAccount;
- function getProjectDefaultAccount(projectDir) {
- if (!projectDir) {
- return getGlobalDefaultAccount();
- }
- const activeAccounts = configstore_1.configstore.get("activeAccounts") || {};
- const email = activeAccounts[projectDir];
- if (!email) {
- return getGlobalDefaultAccount();
- }
- const allAccounts = getAllAccounts();
- return allAccounts.find((a) => a.user.email === email);
- }
- exports.getProjectDefaultAccount = getProjectDefaultAccount;
- function getAdditionalAccounts() {
- return configstore_1.configstore.get("additionalAccounts") || [];
- }
- exports.getAdditionalAccounts = getAdditionalAccounts;
- function getAllAccounts() {
- const res = [];
- const defaultUser = getGlobalDefaultAccount();
- if (defaultUser) {
- res.push(defaultUser);
- }
- res.push(...getAdditionalAccounts());
- return res;
- }
- exports.getAllAccounts = getAllAccounts;
- function setActiveAccount(options, account) {
- if (account.tokens.refresh_token) {
- setRefreshToken(account.tokens.refresh_token);
- }
- options.user = account.user;
- options.tokens = account.tokens;
- }
- exports.setActiveAccount = setActiveAccount;
- function setRefreshToken(token) {
- apiv2.setRefreshToken(token);
- }
- exports.setRefreshToken = setRefreshToken;
- function selectAccount(account, projectRoot) {
- const defaultUser = getProjectDefaultAccount(projectRoot);
- if (!account) {
- return defaultUser;
- }
- if (!defaultUser) {
- throw new error_1.FirebaseError(`Account ${account} not found, have you run "firebase login"?`);
- }
- const matchingAccount = getAllAccounts().find((a) => a.user.email === account);
- if (matchingAccount) {
- return matchingAccount;
- }
- throw new error_1.FirebaseError(`Account ${account} not found, run "firebase login:list" to see existing accounts or "firebase login:add" to add a new one`);
- }
- exports.selectAccount = selectAccount;
- async function loginAdditionalAccount(useLocalhost, email) {
- const result = await loginGoogle(useLocalhost, email);
- if (typeof result.user === "string") {
- throw new error_1.FirebaseError("Failed to parse auth response, see debug log.");
- }
- const resultEmail = result.user.email;
- if (email && resultEmail !== email) {
- utils.logWarning(`Chosen account ${resultEmail} does not match account hint ${email}`);
- }
- const allAccounts = getAllAccounts();
- const newAccount = {
- user: result.user,
- tokens: result.tokens,
- };
- const existingAccount = allAccounts.find((a) => a.user.email === resultEmail);
- if (existingAccount) {
- utils.logWarning(`Already logged in as ${resultEmail}.`);
- updateAccount(newAccount);
- }
- else {
- const additionalAccounts = getAdditionalAccounts();
- additionalAccounts.push(newAccount);
- configstore_1.configstore.set("additionalAccounts", additionalAccounts);
- }
- return newAccount;
- }
- exports.loginAdditionalAccount = loginAdditionalAccount;
- function setProjectAccount(projectDir, email) {
- logger_1.logger.debug(`setProjectAccount(${projectDir}, ${email})`);
- const activeAccounts = configstore_1.configstore.get("activeAccounts") || {};
- activeAccounts[projectDir] = email;
- configstore_1.configstore.set("activeAccounts", activeAccounts);
- }
- exports.setProjectAccount = setProjectAccount;
- function setGlobalDefaultAccount(account) {
- configstore_1.configstore.set("user", account.user);
- configstore_1.configstore.set("tokens", account.tokens);
- const additionalAccounts = getAdditionalAccounts();
- const index = additionalAccounts.findIndex((a) => a.user.email === account.user.email);
- if (index >= 0) {
- additionalAccounts.splice(index, 1);
- configstore_1.configstore.set("additionalAccounts", additionalAccounts);
- }
- }
- exports.setGlobalDefaultAccount = setGlobalDefaultAccount;
- function open(url) {
- opn(url).catch((err) => {
- logger_1.logger.debug("Unable to open URL: " + err.stack);
- });
- }
- function invalidCredentialError() {
- return new error_1.FirebaseError("Authentication Error: Your credentials are no longer valid. Please run " +
- clc.bold("firebase login --reauth") +
- "\n\n" +
- "For CI servers and headless environments, generate a new token with " +
- clc.bold("firebase login:ci"), { exit: 1 });
- }
- const FIFTEEN_MINUTES_IN_MS = 15 * 60 * 1000;
- const SCOPES = [
- scopes.EMAIL,
- scopes.OPENID,
- scopes.CLOUD_PROJECTS_READONLY,
- scopes.FIREBASE_PLATFORM,
- scopes.CLOUD_PLATFORM,
- ];
- const _nonce = Math.floor(Math.random() * (2 << 29) + 1).toString();
- const getPort = portfinder.getPortPromise;
- let lastAccessToken;
- function getCallbackUrl(port) {
- if (typeof port === "undefined") {
- return "urn:ietf:wg:oauth:2.0:oob";
- }
- return `http://localhost:${port}`;
- }
- function queryParamString(args) {
- const tokens = [];
- for (const [key, value] of Object.entries(args)) {
- if (typeof value === "string") {
- tokens.push(key + "=" + encodeURIComponent(value));
- }
- }
- return tokens.join("&");
- }
- function getLoginUrl(callbackUrl, userHint) {
- return (api_1.authOrigin +
- "/o/oauth2/auth?" +
- queryParamString({
- client_id: api_1.clientId,
- scope: SCOPES.join(" "),
- response_type: "code",
- state: _nonce,
- redirect_uri: callbackUrl,
- login_hint: userHint,
- }));
- }
- async function getTokensFromAuthorizationCode(code, callbackUrl, verifier) {
- const params = {
- code: code,
- client_id: api_1.clientId,
- client_secret: api_1.clientSecret,
- redirect_uri: callbackUrl,
- grant_type: "authorization_code",
- };
- if (verifier) {
- params["code_verifier"] = verifier;
- }
- let res;
- try {
- const client = new apiv2.Client({ urlPrefix: api_1.authOrigin, auth: false });
- const form = new FormData();
- for (const [k, v] of Object.entries(params)) {
- form.append(k, v);
- }
- res = await client.request({
- method: "POST",
- path: "/o/oauth2/token",
- body: form,
- headers: form.getHeaders(),
- skipLog: { body: true, queryParams: true, resBody: true },
- });
- }
- catch (err) {
- if (err instanceof Error) {
- logger_1.logger.debug("Token Fetch Error:", err.stack || "");
- }
- else {
- logger_1.logger.debug("Token Fetch Error");
- }
- throw invalidCredentialError();
- }
- if (!res.body.access_token && !res.body.refresh_token) {
- logger_1.logger.debug("Token Fetch Error:", res.status, res.body);
- throw invalidCredentialError();
- }
- lastAccessToken = Object.assign({
- expires_at: Date.now() + res.body.expires_in * 1000,
- }, res.body);
- return lastAccessToken;
- }
- const GITHUB_SCOPES = ["read:user", "repo", "public_repo"];
- function getGithubLoginUrl(callbackUrl) {
- return (api_1.githubOrigin +
- "/login/oauth/authorize?" +
- queryParamString({
- client_id: api_1.githubClientId,
- state: _nonce,
- redirect_uri: callbackUrl,
- scope: GITHUB_SCOPES.join(" "),
- }));
- }
- async function getGithubTokensFromAuthorizationCode(code, callbackUrl) {
- const client = new apiv2.Client({ urlPrefix: api_1.githubOrigin, auth: false });
- const data = {
- client_id: api_1.githubClientId,
- client_secret: api_1.githubClientSecret,
- code,
- redirect_uri: callbackUrl,
- state: _nonce,
- };
- const form = new FormData();
- for (const [k, v] of Object.entries(data)) {
- form.append(k, v);
- }
- const headers = form.getHeaders();
- headers.accept = "application/json";
- const res = await client.request({
- method: "POST",
- path: "/login/oauth/access_token",
- body: form,
- headers,
- });
- return res.body.access_token;
- }
- async function respondWithFile(req, res, statusCode, filename) {
- const response = await util.promisify(fs.readFile)(path.join(__dirname, filename));
- res.writeHead(statusCode, {
- "Content-Length": response.length,
- "Content-Type": "text/html",
- });
- res.end(response);
- req.socket.destroy();
- }
- function urlsafeBase64(base64string) {
- return base64string.replace(/\+/g, "-").replace(/=+$/, "").replace(/\//g, "_");
- }
- async function loginRemotely() {
- var _a;
- const authProxyClient = new apiv2.Client({
- urlPrefix: api_1.authProxyOrigin,
- auth: false,
- });
- const sessionId = (0, uuid_1.v4)();
- const codeVerifier = (0, crypto_1.randomBytes)(32).toString("hex");
- const codeChallenge = urlsafeBase64((0, crypto_1.createHash)("sha256").update(codeVerifier).digest("base64"));
- const attestToken = (_a = (await authProxyClient.post("/attest", {
- session_id: sessionId,
- })).body) === null || _a === void 0 ? void 0 : _a.token;
- const loginUrl = `${api_1.authProxyOrigin}/login?code_challenge=${codeChallenge}&session=${sessionId}&attest=${attestToken}`;
- logger_1.logger.info();
- logger_1.logger.info("To sign in to the Firebase CLI:");
- logger_1.logger.info();
- logger_1.logger.info("1. Take note of your session ID:");
- logger_1.logger.info();
- logger_1.logger.info(` ${clc.bold(sessionId.substring(0, 5).toUpperCase())}`);
- logger_1.logger.info();
- logger_1.logger.info("2. Visit the URL below on any device and follow the instructions to get your code:");
- logger_1.logger.info();
- logger_1.logger.info(` ${loginUrl}`);
- logger_1.logger.info();
- logger_1.logger.info("3. Paste or enter the authorization code below once you have it:");
- logger_1.logger.info();
- const code = await (0, prompt_1.promptOnce)({
- type: "input",
- message: "Enter authorization code:",
- });
- try {
- const tokens = await getTokensFromAuthorizationCode(code, `${api_1.authProxyOrigin}/complete`, codeVerifier);
- void (0, track_1.track)("login", "google_remote");
- return {
- user: jwt.decode(tokens.id_token),
- tokens: tokens,
- scopes: SCOPES,
- };
- }
- catch (e) {
- throw new error_1.FirebaseError("Unable to authenticate using the provided code. Please try again.");
- }
- }
- async function loginWithLocalhostGoogle(port, userHint) {
- const callbackUrl = getCallbackUrl(port);
- const authUrl = getLoginUrl(callbackUrl, userHint);
- const successTemplate = "../templates/loginSuccess.html";
- const tokens = await loginWithLocalhost(port, callbackUrl, authUrl, successTemplate, getTokensFromAuthorizationCode);
- void (0, track_1.track)("login", "google_localhost");
- return {
- user: jwt.decode(tokens.id_token),
- tokens: tokens,
- scopes: tokens.scopes,
- };
- }
- async function loginWithLocalhostGitHub(port) {
- const callbackUrl = getCallbackUrl(port);
- const authUrl = getGithubLoginUrl(callbackUrl);
- const successTemplate = "../templates/loginSuccessGithub.html";
- const tokens = await loginWithLocalhost(port, callbackUrl, authUrl, successTemplate, getGithubTokensFromAuthorizationCode);
- void (0, track_1.track)("login", "google_localhost");
- return tokens;
- }
- async function loginWithLocalhost(port, callbackUrl, authUrl, successTemplate, getTokens) {
- return new Promise((resolve, reject) => {
- const server = http.createServer(async (req, res) => {
- const query = url.parse(`${req.url}`, true).query || {};
- const queryState = query.state;
- const queryCode = query.code;
- if (queryState !== _nonce || typeof queryCode !== "string") {
- await respondWithFile(req, res, 400, "../templates/loginFailure.html");
- reject(new error_1.FirebaseError("Unexpected error while logging in"));
- server.close();
- return;
- }
- try {
- const tokens = await getTokens(queryCode, callbackUrl);
- await respondWithFile(req, res, 200, successTemplate);
- resolve(tokens);
- }
- catch (err) {
- await respondWithFile(req, res, 400, "../templates/loginFailure.html");
- reject(err);
- }
- server.close();
- return;
- });
- server.listen(port, () => {
- logger_1.logger.info();
- logger_1.logger.info("Visit this URL on this device to log in:");
- logger_1.logger.info(clc.bold(clc.underline(authUrl)));
- logger_1.logger.info();
- logger_1.logger.info("Waiting for authentication...");
- open(authUrl);
- });
- server.on("error", (err) => {
- reject(err);
- });
- });
- }
- async function loginGoogle(localhost, userHint) {
- if (localhost) {
- try {
- const port = await getPort();
- return await loginWithLocalhostGoogle(port, userHint);
- }
- catch (_a) {
- return await loginRemotely();
- }
- }
- return await loginRemotely();
- }
- exports.loginGoogle = loginGoogle;
- async function loginGithub() {
- const port = await getPort();
- return loginWithLocalhostGitHub(port);
- }
- exports.loginGithub = loginGithub;
- function findAccountByEmail(email) {
- return getAllAccounts().find((a) => a.user.email === email);
- }
- exports.findAccountByEmail = findAccountByEmail;
- function haveValidTokens(refreshToken, authScopes) {
- var _a;
- if (!(lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.access_token)) {
- const tokens = configstore_1.configstore.get("tokens");
- if (refreshToken === (tokens === null || tokens === void 0 ? void 0 : tokens.refresh_token)) {
- lastAccessToken = tokens;
- }
- }
- const hasTokens = !!(lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.access_token);
- const oldScopesJSON = JSON.stringify(((_a = lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.scopes) === null || _a === void 0 ? void 0 : _a.sort()) || []);
- const newScopesJSON = JSON.stringify(authScopes.sort());
- const hasSameScopes = oldScopesJSON === newScopesJSON;
- const isExpired = ((lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.expires_at) || 0) < Date.now() + FIFTEEN_MINUTES_IN_MS;
- return hasTokens && hasSameScopes && !isExpired;
- }
- function deleteAccount(account) {
- const defaultAccount = getGlobalDefaultAccount();
- if (account.user.email === (defaultAccount === null || defaultAccount === void 0 ? void 0 : defaultAccount.user.email)) {
- configstore_1.configstore.delete("user");
- configstore_1.configstore.delete("tokens");
- configstore_1.configstore.delete("usage");
- configstore_1.configstore.delete("analytics-uuid");
- }
- const additionalAccounts = getAdditionalAccounts();
- const remainingAccounts = additionalAccounts.filter((a) => a.user.email !== account.user.email);
- configstore_1.configstore.set("additionalAccounts", remainingAccounts);
- const activeAccounts = configstore_1.configstore.get("activeAccounts") || {};
- for (const [projectDir, projectAccount] of Object.entries(activeAccounts)) {
- if (projectAccount === account.user.email) {
- delete activeAccounts[projectDir];
- }
- }
- configstore_1.configstore.set("activeAccounts", activeAccounts);
- }
- function updateAccount(account) {
- const defaultAccount = getGlobalDefaultAccount();
- if (account.user.email === (defaultAccount === null || defaultAccount === void 0 ? void 0 : defaultAccount.user.email)) {
- configstore_1.configstore.set("user", account.user);
- configstore_1.configstore.set("tokens", account.tokens);
- }
- const additionalAccounts = getAdditionalAccounts();
- const accountIndex = additionalAccounts.findIndex((a) => a.user.email === account.user.email);
- if (accountIndex >= 0) {
- additionalAccounts.splice(accountIndex, 1, account);
- configstore_1.configstore.set("additionalAccounts", additionalAccounts);
- }
- }
- function findAccountByRefreshToken(refreshToken) {
- return getAllAccounts().find((a) => a.tokens.refresh_token === refreshToken);
- }
- function logoutCurrentSession(refreshToken) {
- const account = findAccountByRefreshToken(refreshToken);
- if (!account) {
- return;
- }
- (0, defaultCredentials_1.clearCredentials)(account);
- deleteAccount(account);
- }
- async function refreshTokens(refreshToken, authScopes) {
- var _a, _b;
- logger_1.logger.debug("> refreshing access token with scopes:", JSON.stringify(authScopes));
- try {
- const client = new apiv2.Client({ urlPrefix: api_1.googleOrigin, auth: false });
- const data = {
- refresh_token: refreshToken,
- client_id: api_1.clientId,
- client_secret: api_1.clientSecret,
- grant_type: "refresh_token",
- scope: (authScopes || []).join(" "),
- };
- const form = new FormData();
- for (const [k, v] of Object.entries(data)) {
- form.append(k, v);
- }
- const res = await client.request({
- method: "POST",
- path: "/oauth2/v3/token",
- body: form,
- headers: form.getHeaders(),
- skipLog: { body: true, queryParams: true, resBody: true },
- resolveOnHTTPError: true,
- });
- if (res.status === 401 || res.status === 400) {
- return { access_token: refreshToken };
- }
- if (typeof res.body.access_token !== "string") {
- throw invalidCredentialError();
- }
- lastAccessToken = Object.assign({
- expires_at: Date.now() + res.body.expires_in * 1000,
- refresh_token: refreshToken,
- scopes: authScopes,
- }, res.body);
- const account = findAccountByRefreshToken(refreshToken);
- if (account && lastAccessToken) {
- account.tokens = lastAccessToken;
- updateAccount(account);
- }
- return lastAccessToken;
- }
- catch (err) {
- if (((_b = (_a = err === null || err === void 0 ? void 0 : err.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.error) === "invalid_scope") {
- throw new error_1.FirebaseError("This command requires new authorization scopes not granted to your current session. Please run " +
- clc.bold("firebase login --reauth") +
- "\n\n" +
- "For CI servers and headless environments, generate a new token with " +
- clc.bold("firebase login:ci"), { exit: 1 });
- }
- throw invalidCredentialError();
- }
- }
- async function getAccessToken(refreshToken, authScopes) {
- if (haveValidTokens(refreshToken, authScopes)) {
- return lastAccessToken;
- }
- return refreshTokens(refreshToken, authScopes);
- }
- exports.getAccessToken = getAccessToken;
- async function logout(refreshToken) {
- if ((lastAccessToken === null || lastAccessToken === void 0 ? void 0 : lastAccessToken.refresh_token) === refreshToken) {
- lastAccessToken = undefined;
- }
- logoutCurrentSession(refreshToken);
- try {
- const client = new apiv2.Client({ urlPrefix: api_1.authOrigin, auth: false });
- await client.get("/o/oauth2/revoke", { queryParams: { token: refreshToken } });
- }
- catch (thrown) {
- const err = thrown instanceof Error ? thrown : new Error(thrown);
- throw new error_1.FirebaseError("Authentication Error.", {
- exit: 1,
- original: err,
- });
- }
- }
- exports.logout = logout;
|