token expiry"

"
This commit is contained in:
ntr 2019-06-16 15:06:43 +10:00
parent 2b06c83ea0
commit 9fcdbeb370
5 changed files with 57 additions and 50 deletions

View File

@ -10,8 +10,8 @@ const List = require('./list');
const addState = connect( const addState = connect(
state => { state => {
const { game, instance, account, nav, team } = state; const { game, instance, account, nav, team, constructs } = state;
return { game, instance, account, nav, team }; return { game, instance, account, nav, team, constructs };
} }
); );
@ -22,6 +22,7 @@ function Main(props) {
account, account,
nav, nav,
team, team,
constructs,
} = props; } = props;
if (!account) { if (!account) {
@ -36,8 +37,8 @@ function Main(props) {
return <Instance />; return <Instance />;
} }
if (nav === 'team' || !team.some(t => t) || constructs.length < 3) return <Team />;
if (nav === 'list') return <List />; if (nav === 'list') return <List />;
if (nav === 'team' || !team.some(t => t)) return <Team />;
return ( return (
<main></main> <main></main>

View File

@ -4,7 +4,8 @@ exports.up = async knex => {
table.timestamps(true, true); table.timestamps(true, true);
table.string('name', 42).notNullable().unique(); table.string('name', 42).notNullable().unique();
table.string('password').notNullable(); table.string('password').notNullable();
table.string('token', 64).notNullable(); table.string('token', 64);
table.timestamp('token_expiry');
table.index('name'); table.index('name');
table.index('id'); table.index('id');

View File

@ -29,13 +29,12 @@ struct AccountEntry {
token: String, token: String,
} }
// MAYBE
// hash tokens with a secret
pub fn account_from_token(token: String, tx: &mut Transaction) -> Result<Account, Error> { pub fn account_from_token(token: String, tx: &mut Transaction) -> Result<Account, Error> {
let query = " let query = "
SELECT id, name, token SELECT id, name, token
FROM accounts FROM accounts
WHERE token = $1; WHERE token = $1
AND token_expiry > now();
"; ";
let result = tx let result = tx
@ -71,78 +70,93 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut
let rounds = 8; let rounds = 8;
let password = hash(&password, rounds)?; let password = hash(&password, rounds)?;
let mut rng = thread_rng();
let token: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(64)
.collect();
let query = " let query = "
INSERT INTO accounts (id, name, password, token) INSERT INTO accounts (id, name, password)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3)
RETURNING id, name, token; RETURNING id, name;
"; ";
let result = tx let result = tx
.query(query, &[&id, &name, &password, &token])?; .query(query, &[&id, &name, &password])?;
if result.is_empty() { let returned = match result.iter().next() {
return Err(err_msg("no row returned")); Some(row) => row,
} None => return Err(err_msg("account not created")),
};
info!("registration account={:?}", name); info!("registration account={:?}", name);
return Ok(token); let account = Account {
id: returned.get(0),
name: returned.get(1),
};
account_set_token(tx, &account)
} }
pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result<String, Error> { pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result<String, Error> {
let query = " let query = "
SELECT id, password SELECT id, password, name
FROM accounts FROM accounts
WHERE name = $1; WHERE name = $1
RETURNING id, name;
"; ";
let result = tx let result = tx
.query(query, &[&name])?; .query(query, &[&name])?;
let mut rng = thread_rng();
let token: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(64)
.collect();
let returned = match result.iter().next() { let returned = match result.iter().next() {
Some(row) => row, Some(row) => row,
None => { None => {
let mut rng = thread_rng();
let garbage: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(64)
.collect();
// verify garbage to prevent timing attacks // verify garbage to prevent timing attacks
verify(token.clone(), &token).ok(); verify(garbage.clone(), &garbage).ok();
return Err(err_msg("account not found")); return Err(err_msg("account not found"));
}, },
}; };
let id: Uuid = returned.get(0); let account = Account {
id: returned.get(0),
name: returned.get(2),
};
let hash: String = returned.get(1); let hash: String = returned.get(1);
if !verify(password, &hash)? { if !verify(password, &hash)? {
return Err(err_msg("password does not match")); return Err(err_msg("password does not match"));
} }
account_set_token(tx, &account)
}
fn account_set_token(tx: &mut Transaction, account: &Account) -> Result<String, Error> {
let mut rng = thread_rng();
let token: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(64)
.collect();
// update token // update token
let query = " let query = "
UPDATE accounts UPDATE accounts
SET token = $1, updated_at = now() SET token = $1, updated_at = now(), token_expiry = now() + interval '1 week'
WHERE id = $2 WHERE id = $2
RETURNING id; RETURNING id;
"; ";
let result = tx let result = tx
.query(query, &[&token, &id])?; .query(query, &[&token, &account.id])?;
result.iter().next().ok_or(format_err!("user {:?} could not be updated", id))?; result.iter().next().ok_or(format_err!("user {:?} could not be updated", account.id))?;
info!("login account={:?}", name); info!("login account={:?}", account.name);
return Ok(token); Ok(token)
} }
pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, Error> { pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, Error> {

View File

@ -26,11 +26,7 @@ const DB_POOL_SIZE: u32 = 20;
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
/// websocket connection is long running connection, it easier
/// to handle with an actor
pub struct MnmlSocket { pub struct MnmlSocket {
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
/// otherwise we drop connection.
hb: Instant, hb: Instant,
pool: PgPool, pool: PgPool,
account: Option<Account>, account: Option<Account>,
@ -170,11 +166,10 @@ fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Res
fn token_res(token: String, secure: bool) -> HttpResponse { fn token_res(token: String, secure: bool) -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.cookie(Cookie::build("x-auth-token", token) .cookie(Cookie::build("x-auth-token", token)
// .path("/") .secure(secure)
.secure(secure) // needs to be enabled for prod
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.max_age(60 * 60 * 24 * 7) // 1 week .max_age(60 * 60 * 24 * 7) // 1 week aligns with db set
.finish()) .finish())
.finish() .finish()
} }

View File

@ -29,7 +29,7 @@ pub fn receive(data: Vec<u8>, db: &Db, _client: &mut MnmlWs, begin: Instant, acc
let mut tx = db.transaction()?; let mut tx = db.transaction()?;
let account_name = match &account { let account_name = match account {
Some(a) => a.name.clone(), Some(a) => a.name.clone(),
None => "none".to_string(), None => "none".to_string(),
}; };
@ -37,9 +37,7 @@ pub fn receive(data: Vec<u8>, db: &Db, _client: &mut MnmlWs, begin: Instant, acc
// check the method // check the method
// if no auth required // if no auth required
match v.method.as_ref() { match v.method.as_ref() {
"account_create" => (), "item_info" => return Ok(RpcResult::ItemInfo(item_info())),
// "account_login" => (),
"item_info" => (),
_ => match account { _ => match account {
Some(_) => (), Some(_) => (),
None => return Err(err_msg("auth required")), None => return Err(err_msg("auth required")),
@ -51,9 +49,7 @@ pub fn receive(data: Vec<u8>, db: &Db, _client: &mut MnmlWs, begin: Instant, acc
// now we have the method name // now we have the method name
// match on that to determine what fn to call // match on that to determine what fn to call
let response = match v.method.as_ref() { let response = match v.method.as_ref() {
"item_info" => Ok(RpcResult::ItemInfo(item_info())), "account_state" => return Ok(RpcResult::AccountState(account.clone())),
"account_state" => Ok(RpcResult::AccountState(account.clone())),
"account_constructs" => handle_account_constructs(data, &mut tx, account), "account_constructs" => handle_account_constructs(data, &mut tx, account),
"account_instances" => handle_account_instances(data, &mut tx, account), "account_instances" => handle_account_instances(data, &mut tx, account),