Skip to content

Commit

Permalink
518 secure server session (steemit#823)
Browse files Browse the repository at this point in the history
* Adds authentication proof for server. (close steemit#518)

* Rename fromm koa-crypto-session to crypto-session

* some more merging updates..

* make sure logged in account is used in notifications

* remove config.login_challenge_description

* depricate login_challenge call; don't call login_account if already logged in

* Sign login challenge with posting key only (active commented out). steemit#518

* remove active verification from session.auth (probably temp)

* Fix challenge string sigining.

* remove some debug output

* remove some commented out code

* remove session.auth, use session.login_challenge instead, pass login_challenge as single token

* Small login challenge data-structure fix.

* Notifications api warning if not logged in.

* Notifications api warning if not logged in.

* Adjust notification api warning if not logged in

* should make it call /login_account even if page wasn't refreshed after logout

* small code rearrangment
  • Loading branch information
Valentine Zavgorodnev authored Dec 9, 2016
1 parent 8c0f1cb commit 611370f
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 25 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ npm install -g babel-cli

#### Create config file


```bash
cd config
cp steem-example.json steem-dev.json
```

Generate a new crypto_key and save under server_session_secret in ./steem-dev.json.

```bash
node
> crypto.randomBytes(32).toString('base64')
```

(note: it's steem.json in production)

#### Install mysql server
Expand Down
15 changes: 8 additions & 7 deletions app/redux/Offchain.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Immutable from 'immutable';
import createModule from 'redux-modules';
import {PropTypes} from 'react';

const {string, object, shape, oneOf} = PropTypes;
const defaultState = Immutable.fromJS({user: {}});

export default createModule({
name: 'offchain',
initialState: defaultState,
transformations: []
});
export default function reducer(state = defaultState, action) {
if (action.type === 'user/SAVE_LOGIN_CONFIRM') {
if (!action.payload) {
state = state.set('account', null);
}
}
return state;
}
2 changes: 1 addition & 1 deletion app/redux/RootReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function initReducer(reducer, type) {
export default combineReducers({
global: initReducer(globalReducerModule.reducer, 'global'),
market: initReducer(marketReducerModule.reducer),
offchain: initReducer(offchain.reducer),
offchain: initReducer(offchain),
user: initReducer(user.reducer),
// auth: initReducer(auth.reducer),
transaction: initReducer(transaction.reducer),
Expand Down
27 changes: 23 additions & 4 deletions app/redux/UserSaga.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {fromJS, Set, List} from 'immutable'
import {takeLatest} from 'redux-saga';
import {call, put, select, fork} from 'redux-saga/effects';
import {accountAuthLookup} from 'app/redux/AuthSaga'
import {PrivateKey} from 'shared/ecc'
import {PrivateKey, Signature, hash} from 'shared/ecc'
import user from 'app/redux/User'
import {getAccount} from 'app/redux/SagaShared'
import {browserHistory} from 'react-router'
Expand Down Expand Up @@ -246,7 +246,28 @@ function* usernamePasswordLogin2({payload: {username, password, saveLogin,
if (!autopost && saveLogin)
yield put(user.actions.saveLogin());

serverApiLogin(username);
try {
// const challengeString = yield serverApiLoginChallenge()
const offchainData = yield select(state => state.offchain)
const serverAccount = offchainData.get('account')
const challengeString = offchainData.get('login_challenge')
if (!serverAccount && challengeString) {
const signatures = {}
const challenge = {token: challengeString}
const bufSha = hash.sha256(JSON.stringify(challenge, null, 0))
const sign = (role, d) => {
if (!d) return
const sig = Signature.signBufferSha256(bufSha, d)
signatures[role] = sig.toHex()
}
sign('posting', private_keys.get('posting_private'))
// sign('active', private_keys.get('active_private'))
serverApiLogin(username, signatures);
}
} catch(error) {
// Does not need to be fatal
console.error('Server Login Error', error);
}
if (afterLoginRedirectToWelcome) browserHistory.push('/welcome');
}

Expand Down Expand Up @@ -347,8 +368,6 @@ function* lookupPreviousOwnerAuthority({payload: {}}) {
yield put(user.actions.setUser({previous_owner_authority}))
}

import {Signature, hash} from 'shared/ecc'

function* uploadImageWatch() {
yield* takeLatest('user/UPLOAD_IMAGE', uploadImage);
}
Expand Down
5 changes: 3 additions & 2 deletions app/utils/ServerApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const request_base = {
}
};

export function serverApiLogin(account) {
export function serverApiLogin(account, signatures) {
if (!process.env.BROWSER || window.$STM_ServerBusy) return;
const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, account})});
const request = Object.assign({}, request_base, {body: JSON.stringify({account, signatures, csrf: $STM_csrf})});
fetch('/api/v1/login_account', request);
}

Expand Down Expand Up @@ -69,3 +69,4 @@ if (process.env.BROWSER) {
window.getNotifications = getNotifications;
window.markNotificationRead = markNotificationRead;
}

1 change: 1 addition & 0 deletions config/steem-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
]
}
},
"server_session_secret": "",
"helmet": {},
"registrar": {
"account": "-",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"koa-route": "^2.4.2",
"koa-router": "^5.4.0",
"koa-session": "^3.3.1",
"@steem/crypto-session": "git+https://github.com/steemit/crypto-session.git",
"koa-static-cache": "^3.1.2",
"lodash.debounce": "^4.0.7",
"medium-editor-insert-plugin": "^2.3.2",
Expand Down
42 changes: 38 additions & 4 deletions server/api/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import recordWebEvent from 'server/record_web_event';
import {esc, escAttrs} from 'db/models';
import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils';
import coBody from 'co-body';
import secureRandom from 'secure-random'
import {PublicKey, Signature, hash} from 'shared/ecc'
import Mixpanel from 'mixpanel';
import Tarantool from 'db/tarantool';

const mixpanel = config.mixpanel ? Mixpanel.init(config.mixpanel) : null;


export default function useGeneralApi(app) {
const router = koa_router({prefix: '/api/v1'});
app.use(router.routes());
Expand Down Expand Up @@ -194,20 +195,51 @@ export default function useGeneralApi(app) {
router.post('/login_account', koaBody, function *() {
if (rateLimitReq(this, this.req)) return;
const params = this.request.body;
const {csrf, account} = typeof(params) === 'string' ? JSON.parse(params) : params;
const {csrf, account, signatures} = typeof(params) === 'string' ? JSON.parse(params) : params;
if (!checkCSRF(this, csrf)) return;
console.log('-- /login_account -->', this.session.uid, account);
try {
this.session.a = account;
const db_account = yield models.Account.findOne(
{attributes: ['user_id'], where: {name: esc(account)}, logging: false}
);
if (db_account) this.session.user = db_account.user_id;

if(signatures) {
if(!this.session.login_challenge) {
console.error('/login_account missing this.session.login_challenge');
} else {
const [chainAccount] = yield Apis.db_api('get_accounts', [account])
if(!chainAccount) {
console.error('/login_account missing blockchain account', account);
} else {
const auth = {posting: false}
const bufSha = hash.sha256(JSON.stringify({token: this.session.login_challenge}, null, 0))
const verify = (type, sigHex, pubkey, weight, weight_threshold) => {
if(!sigHex) return
if(weight !== 1 || weight_threshold !== 1) {
console.error(`/login_account login_challenge unsupported ${type} auth configuration: ${account}`);
} else {
const sig = parseSig(sigHex)
const public_key = PublicKey.fromString(pubkey)
const verified = sig.verifyHash(bufSha, public_key)
if (!verified) {
console.error('/login_account verification failed', this.session.uid, account, pubkey)
}
auth[type] = verified
}
}
const {posting: {key_auths: [[posting_pubkey, weight]], weight_threshold}} = chainAccount
verify('posting', signatures.posting, posting_pubkey, weight, weight_threshold)
if (auth.posting) this.session.a = account;
}
}
}

this.body = JSON.stringify({status: 'ok'});
const remote_ip = getRemoteIp(this.req);
if (mixpanel) {
mixpanel.people.set(this.session.uid, {ip: remote_ip, $ip: remote_ip});
mixpanel.people.increment(this.session.uid, 'Visits', 1);
mixpanel.people.increment(this.session.uid, 'Logins', 1);
}
} catch (error) {
console.error('Error in /login_account api call', this.session.uid, error.message);
Expand Down Expand Up @@ -348,3 +380,5 @@ function* createAccount({
Apis.broadcastTransaction(sx, () => {resolve()}).catch(e => {reject(e)})
)
}

const parseSig = hexSig => {try {return Signature.fromHex(hexSig)} catch(e) {return null}}
17 changes: 13 additions & 4 deletions server/api/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ export default function useNotificationsApi(app) {
// get all notifications for account
router.get('/notifications/:account', function *() {
const account = this.params.account;
// TODO: make sure account name matches session
console.log('-- GET /notifications/:account -->', account);
console.log('-- GET /notifications/:account -->', account, status(this, account));

if (!account || account !== this.session.a) {
this.body = []; return;
}
try {
const res = yield Tarantool.instance().select('notifications', 0, 1, 0, 'eq', account);
this.body = toResArray(res);
Expand All @@ -29,10 +32,11 @@ export default function useNotificationsApi(app) {
// mark account's notification as read
router.put('/notifications/:account/:ids', function *() {
const {account, ids} = this.params;
if (!ids) {
console.log('-- PUT /notifications/:account/:id -->', account, status(this, account));

if (!ids || !account || account !== this.session.a) {
this.body = []; return;
}
console.log('-- PUT /notifications/:account/:id -->', account, ids);
const fields = ids.split('-');
try {
let res;
Expand All @@ -47,3 +51,8 @@ export default function useNotificationsApi(app) {
return;
});
}

const status = (ctx, account) =>
ctx.session.a == null ? 'not logged in' :
account !== ctx.session.a ? 'wrong account' + ctx.session.a :
''
9 changes: 8 additions & 1 deletion server/app_render.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import { renderToString } from 'react-dom/server';
import ServerHTML from './server-html';
import universalRender from '../shared/UniversalRender';
import models from 'db/models';
import secureRandom from 'secure-random'

const DB_RECONNECT_TIMEOUT = process.env.NODE_ENV === 'development' ? 1000 * 60 * 60 : 1000 * 60 * 10;

async function appRender(ctx) {
const store = {};
try {
let login_challenge = ctx.session.login_challenge;
if (!login_challenge) {
login_challenge = secureRandom.randomBuffer(16).toString('hex');
ctx.session.login_challenge = login_challenge;
}
const offchain = {
csrf: ctx.csrf,
flash: ctx.flash,
new_visit: ctx.session.new_visit,
account: ctx.session.a,
config: $STM_Config
config: $STM_Config,
login_challenge
};
const user_id = ctx.session.user;
if (user_id) {
Expand Down
6 changes: 4 additions & 2 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import useNotificationsApi from './api/notifications';
import useEnterAndConfirmEmailPages from './server_pages/enter_confirm_email';
import useEnterAndConfirmMobilePages from './server_pages/enter_confirm_mobile';
import isBot from 'koa-isbot';
import session from 'koa-session';
import session from '@steem/crypto-session';
import csrf from 'koa-csrf';
import flash from 'koa-flash';
import minimist from 'minimist';
Expand All @@ -33,8 +33,10 @@ const env = process.env.NODE_ENV || 'development';
const cacheOpts = {maxAge: 86400000, gzip: true};

app.keys = [config.session_key];
app.use(session({maxAge: 1000 * 3600 * 24 * 60}, app));
const crypto_key = config.server_session_secret;
session(app, {maxAge: 1000 * 3600 * 24 * 60, crypto_key});
csrf(app);

app.use(mount(grant));
app.use(flash({key: 'flash'}));

Expand Down

0 comments on commit 611370f

Please sign in to comment.