You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
535 lines
21 KiB
535 lines
21 KiB
"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;
|