store auth token in OS keyring, not in local storage

This commit is contained in:
Alex Grintsvayg 2017-06-05 12:29:17 -04:00
parent d4e572a2b6
commit 4376c34678
9 changed files with 142 additions and 135 deletions

View file

@ -13,6 +13,10 @@ cd ..
# build ui # build ui
cd ui cd ui
npm install npm install
# necessary to ensure native Node modules (e.g. keytar) are built against the correct version of Node)
node_modules\.bin\electron-rebuild
node extractLocals.js node extractLocals.js
node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\
node_modules\.bin\webpack node_modules\.bin\webpack

View file

@ -45,6 +45,13 @@ if [ "$FULL_BUILD" == "true" ]; then
python "$BUILD_DIR/set_version.py" python "$BUILD_DIR/set_version.py"
fi fi
libsecret="libsecret-1-dev"
if [ $LINUX -a -z "$(dpkg-query --show --showformat='${Status}\n' "$libsecret" 2>/dev/null | grep "install ok installed")" ]; then
# this is needed for keytar, which does secure password/token management
sudo apt-get install "$libsecret"
fi
[ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist" [ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist"
mkdir -p "$ROOT/dist" mkdir -p "$ROOT/dist"
[ -d "$ROOT/app/dist" ] && rm -rf "$ROOT/app/dist" [ -d "$ROOT/app/dist" ] && rm -rf "$ROOT/app/dist"
@ -61,6 +68,11 @@ npm install
( (
cd "$ROOT/ui" cd "$ROOT/ui"
npm install npm install
# necessary to ensure native Node modules (e.g. keytar) are built against the correct version of Node)
# DEBUG=electron-rebuild node_modules/.bin/electron-rebuild .
node_modules/.bin/electron-rebuild "$ROOT/ui"
node extractLocals.js node extractLocals.js
node_modules/.bin/node-sass --output dist/css --sourcemap=none scss/ node_modules/.bin/node-sass --output dist/css --sourcemap=none scss/
node_modules/.bin/webpack node_modules/.bin/webpack

View file

@ -33,20 +33,20 @@ export function doUserFetch() {
dispatch({ dispatch({
type: types.USER_FETCH_STARTED, type: types.USER_FETCH_STARTED,
}); });
lbryio.setCurrentUser( lbryio
user => { .getCurrentUser()
.then(user => {
dispatch({ dispatch({
type: types.USER_FETCH_SUCCESS, type: types.USER_FETCH_SUCCESS,
data: { user }, data: { user },
}); });
}, })
error => { .catch(error => {
dispatch({ dispatch({
type: types.USER_FETCH_FAILURE, type: types.USER_FETCH_FAILURE,
data: { error }, data: { error },
}); });
} });
);
}; };
} }
@ -56,32 +56,27 @@ export function doUserEmailNew(email) {
type: types.USER_EMAIL_NEW_STARTED, type: types.USER_EMAIL_NEW_STARTED,
email: email, email: email,
}); });
lbryio.call("user_email", "new", { email }, "post").then( lbryio
() => { .call("user_email", "new", { email: email, send_verification_email: true }, "post")
.catch(error => {
if (error.xhr && error.xhr.status == 409) {
return lbryio.call("user_email", "resend_token", { email: email, only_if_expired: true }, "post");
}
throw error
})
.then(() => {
dispatch({ dispatch({
type: types.USER_EMAIL_NEW_SUCCESS, type: types.USER_EMAIL_NEW_SUCCESS,
data: { email }, data: { email },
}); });
dispatch(doUserFetch()); dispatch(doUserFetch());
}, })
error => { .catch(error => {
if (
error.xhr &&
(error.xhr.status == 409 ||
error.message == "This email is already in use")
) {
dispatch({
type: types.USER_EMAIL_NEW_EXISTS,
data: { email },
});
} else {
dispatch({ dispatch({
type: types.USER_EMAIL_NEW_FAILURE, type: types.USER_EMAIL_NEW_FAILURE,
data: { error: error.message }, data: { error: error.message },
}); });
} });
}
);
}; };
} }
@ -111,12 +106,7 @@ export function doUserEmailVerify(verificationToken) {
}; };
lbryio lbryio
.call( .call("user_email", "confirm", { verification_token: verificationToken, email: email }, "post")
"user_email",
"confirm",
{ verification_token: verificationToken, email: email },
"post"
)
.then(userEmail => { .then(userEmail => {
if (userEmail.is_verified) { if (userEmail.is_verified) {
dispatch({ dispatch({

View file

@ -1,26 +1,23 @@
import { getSession, setSession, setLocal } from "./utils.js";
import lbry from "./lbry.js"; import lbry from "./lbry.js";
const querystring = require("querystring"); const querystring = require("querystring");
const keytar = require("keytar");
const lbryio = { const lbryio = {
_accessToken: getSession("accessToken"),
_authenticationPromise: null,
enabled: true, enabled: true,
_authenticationPromise: null,
_exchangePromise: null,
_exchangeLastFetched: null,
}; };
const CONNECTION_STRING = process.env.LBRY_APP_API_URL const CONNECTION_STRING = process.env.LBRY_APP_API_URL
? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end ? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end
: "https://api.lbry.io/"; : "https://api.lbry.io/";
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio._exchangePromise = null;
lbryio._exchangeLastFetched = null;
lbryio.getExchangeRates = function() { lbryio.getExchangeRates = function() {
if ( if (!lbryio._exchangeLastFetched || Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) {
!lbryio._exchangeLastFetched ||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
) {
lbryio._exchangePromise = new Promise((resolve, reject) => { lbryio._exchangePromise = new Promise((resolve, reject) => {
lbryio lbryio
.call("lbc", "exchange_rate", {}, "get", true) .call("lbc", "exchange_rate", {}, "get", true)
@ -46,9 +43,7 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.addEventListener("error", function(event) { xhr.addEventListener("error", function(event) {
reject( reject(new Error(__("Something went wrong making an internal API call.")));
new Error(__("Something went wrong making an internal API call."))
);
}); });
xhr.addEventListener("timeout", function() { xhr.addEventListener("timeout", function() {
@ -60,7 +55,7 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
if (!response.success) { if (!response.success) {
if (reject) { if (reject) {
let error = new Error(response.error); const error = new Error(response.error);
error.xhr = xhr; error.xhr = xhr;
reject(error); reject(error);
} else { } else {
@ -81,24 +76,13 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
} }
}); });
// For social media auth: lbryio
//const accessToken = localStorage.getItem('accessToken'); .getAuthToken()
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; .then(token => {
const fullParams = { auth_token: token, ...params };
// Temp app ID based auth:
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
if (method == "get") { if (method == "get") {
xhr.open( xhr.open("get", CONNECTION_STRING + resource + "/" + action + "?" + querystring.stringify(fullParams), true);
"get",
CONNECTION_STRING +
resource +
"/" +
action +
"?" +
querystring.stringify(fullParams),
true
);
xhr.send(); xhr.send();
} else if (method == "post") { } else if (method == "post") {
xhr.open("post", CONNECTION_STRING + resource + "/" + action, true); xhr.open("post", CONNECTION_STRING + resource + "/" + action, true);
@ -107,30 +91,25 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
} else { } else {
reject(new Error(__("Invalid method"))); reject(new Error(__("Invalid method")));
} }
});
};
lbryio.getAccessToken = () => {
const token = getSession("accessToken");
return token ? token.toString().trim() : token;
};
lbryio.setAccessToken = token => {
setSession("accessToken", token ? token.toString().trim() : token);
};
lbryio.setCurrentUser = (resolve, reject) => {
lbryio
.call("user", "me")
.then(data => {
resolve(data);
}) })
.catch(function(err) { .catch(reject);
lbryio.setAccessToken(null);
reject(err);
}); });
}; };
lbryio.getAuthToken = () => {
return keytar.getPassword("LBRY", "auth_token").then(token => {
return token ? token.toString().trim() : null;
});
};
lbryio.setAuthToken = token => {
return keytar.setPassword("LBRY", "auth_token", token ? token.toString().trim() : null);
};
lbryio.getCurrentUser = () => {
return lbryio.call("user", "me");
};
lbryio.authenticate = function() { lbryio.authenticate = function() {
if (!lbryio.enabled) { if (!lbryio.enabled) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -144,48 +123,56 @@ lbryio.authenticate = function() {
}); });
}); });
} }
if (lbryio._authenticationPromise === null) { if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => { lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry
.status()
.then(response => {
let installation_id = response.installation_id;
if (!lbryio.getAccessToken()) {
lbryio lbryio
.call( .getAuthToken()
"user", .then(token => {
"new", if (!token || token.length > 60)
{ {
language: "en", return false;
app_id: installation_id,
},
"post"
)
.then(function(responseData) {
if (!responseData.id) {
reject(
new Error("Received invalid authentication response.")
);
} }
lbryio.setAccessToken(installation_id);
lbryio.setCurrentUser(resolve, reject); // check that token works
return lbryio
.getCurrentUser()
.then(() => { return true; })
.catch(() => { return false; });
}) })
.catch(function(error) { .then((isTokenValid) => {
/* if (isTokenValid) {
until we have better error code format, assume all errors are duplicate application id return;
if we're wrong, this will be caught by later attempts to make a valid call }
*/
lbryio.setAccessToken(installation_id); let app_id;
lbryio.setCurrentUser(resolve, reject);
return lbry
.status()
.then(status => {
// first try swapping
app_id = status.installation_id;
return lbryio.call("user", "token_swap", {auth_token: "", app_id: app_id}, "post");
})
.catch((err) => {
if (err.xhr.status == 403) {
// cannot swap. either app_id doesn't exist, or app_id already swapped. pretend its the former and create a new user. if we get another error, then its the latter
return lbryio.call("user", "new", {auth_token: "", language: "en", app_id: app_id}, "post");
}
throw err;
})
.then(response => {
if (!response.auth_token) {
throw new Error(__("auth_token is missing from response"));
}
return lbryio.setAuthToken(response.auth_token);
}); });
} else {
lbryio.setCurrentUser(resolve, reject);
}
}) })
.catch(reject); .then(lbryio.getCurrentUser())
.then(resolve, reject);
}); });
} }
return lbryio._authenticationPromise; return lbryio._authenticationPromise;
}; };

View file

@ -69,7 +69,7 @@ export const selectUserIsVerificationCandidate = createSelector(
selectEmailToVerify, selectEmailToVerify,
selectUser, selectUser,
(isEligible, isApproved, emailToVerify, user) => (isEligible, isApproved, emailToVerify, user) =>
(isEligible && !isApproved) || (emailToVerify && user && !user.has_email) emailToVerify && user
); );
export const selectUserIsAuthRequested = createSelector( export const selectUserIsAuthRequested = createSelector(

View file

@ -16,11 +16,11 @@
"name": "LBRY Inc.", "name": "LBRY Inc.",
"email": "hello@lbry.io" "email": "hello@lbry.io"
}, },
"license": "SEE LICENSE IN LICENSE.md", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/lbryio/lbry-app/issues" "url": "https://github.com/lbryio/lbry-app/issues"
}, },
"homepage": "https://github.com/lbryio/lbry-app#readme", "homepage": "https://github.com/lbryio/lbry-app",
"dependencies": { "dependencies": {
"babel-cli": "^6.11.4", "babel-cli": "^6.11.4",
"babel-preset-es2015": "^6.13.2", "babel-preset-es2015": "^6.13.2",
@ -28,6 +28,7 @@
"from2": "^2.3.0", "from2": "^2.3.0",
"jshashes": "^1.0.6", "jshashes": "^1.0.6",
"localforage": "^1.5.0", "localforage": "^1.5.0",
"keytar": "^4.0.3",
"node-sass": "^3.8.0", "node-sass": "^3.8.0",
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^15.4.0", "react": "^15.4.0",
@ -54,6 +55,7 @@
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0", "babel-preset-react": "^6.16.0",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"electron-rebuild": "^1.5.11",
"eslint": "^3.10.2", "eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0", "eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1", "eslint-loader": "^1.6.1",
@ -64,6 +66,7 @@
"i18n-extract": "^0.4.4", "i18n-extract": "^0.4.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lint-staged": "^3.6.0", "lint-staged": "^3.6.0",
"node-loader": "^0.6.0",
"node-sass": "^3.13.0", "node-sass": "^3.13.0",
"prettier": "^1.4.2", "prettier": "^1.4.2",
"webpack": "^2.6.1", "webpack": "^2.6.1",

View file

@ -7,6 +7,8 @@ set -euo pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
(
cd "$DIR"
mkdir -p $DIR/dist/css mkdir -p $DIR/dist/css
mkdir -p $DIR/dist/js mkdir -p $DIR/dist/js
@ -16,7 +18,8 @@ if [ ! -d "$DIR/node_modules" ]; then
fi fi
# run sass once without --watch to force update. then run with --watch to keep watching # run sass once without --watch to force update. then run with --watch to keep watching
$DIR/node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none $DIR/scss/ node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none $DIR/scss/
$DIR/node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none --watch $DIR/scss/ & node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none --watch $DIR/scss/ &
node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch
)

View file

@ -33,6 +33,10 @@ module.exports = {
// define an include so we check just the files we need // define an include so we check just the files we need
include: PATHS.app include: PATHS.app
}, },
{
test: /\.node$/,
use: ["node-loader"]
},
{ {
test: /\.css$/, test: /\.css$/,
use: ["style-loader", "css-loader"] use: ["style-loader", "css-loader"]

View file

@ -41,6 +41,10 @@ module.exports = {
// define an include so we check just the files we need // define an include so we check just the files we need
include: PATHS.app include: PATHS.app
}, },
{
test: /\.node$/,
use: ["node-loader"]
},
{ {
test: /\.css$/, test: /\.css$/,
use: ["style-loader", "css-loader"] use: ["style-loader", "css-loader"]