Skip to content

TOTP Two-factor authentication support #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Ladder} from './ladder';
import {Replays} from './replays';
import {ActionError, QueryHandler} from './server';
import {toID, updateserver, bash, time} from './utils';
import {generateSecret} from '2fa-util';
import * as tables from './tables';
import * as pathModule from 'path';
import IPTools from './ip-tools';
Expand Down Expand Up @@ -88,7 +89,7 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError(`incorrect login data, userid must contain at least one letter or number`);
}
const challengekeyid = parseInt(params.challengekeyid!) || -1;
const actionsuccess = await this.session.login(params.name, params.pass);
const actionsuccess = await this.session.login(params.name, params.pass, params.mfa);
if (!actionsuccess) return {actionsuccess, assertion: false};
const challenge = params.challstr || params.challenge || "";
const assertion = await this.session.getAssertion(
Expand Down Expand Up @@ -263,6 +264,75 @@ export const actions: {[k: string]: QueryHandler} = {
);
},

async request2fa(params) {
const challengeprefix = this.verifyCrossDomainRequest();
const res: {[k: string]: any} = {};
const curuser = this.user;

let userid = '';
if (curuser.loggedin) {
res.username = curuser.name;
userid = curuser.id;
} else if (this.cookies.get('showdown_username')) {
res.username = this.cookies.get('showdown_username')!;
userid = toID(res.username);
}
let assertion = '';
if (userid !== '') {
const challengekeyid = !params.challengekeyid ? -1 : parseInt(params.challengekeyid);
const challenge = params.challstr || "";
assertion = await this.session.getAssertion(
userid, challengekeyid, curuser, challenge, challengeprefix
);
}

const alreadyhas2fa = await tables.users.get(['mfaenabled'], userid).catch(() => {});
if (alreadyhas2fa?.mfaenabled === 1) return false;

const token = await generateSecret(userid, 'Pokemon Showdown!')

const actionsuccess = await tables.users.update(userid, {
mfatoken: token.secret,
}).catch(() => false);

if (!actionsuccess) return false;

if (!assertion.startsWith(';')) {
return {
token: token
};
} else {
return false;
}
},

async confirm2fa(params) {
this.setPrefix('');
const challengeprefix = this.verifyCrossDomainRequest();
if (this.request.method !== 'POST') {
throw new ActionError(`For security reasons, logins must happen with POST data.`);
}
if (!params.name || !params.pass) {
throw new ActionError(`incorrect login data, you need "name" and "pass" fields`);
}
const userid = toID(params.name);
const challengekeyid = parseInt(params.challengekeyid) || -1;
let actionsuccess = await this.session.passwordVerify(params.name, params.pass);
if (!actionsuccess) return {actionsuccess, assertion: false};
actionsuccess = await this.session.mfaVerify(userid, params.mfa);
if (!actionsuccess) return {actionsuccess, assertion: false};
const challenge = params.challstr || "";
const assertion = await this.session.getAssertion(
params.name, challengekeyid, null, challenge, challengeprefix
);
if (!assertion.startsWith(';') && assertion !== ';;mfa') {
let actionsuccess_update = await tables.users.update(userid, {
mfaenabled: 1,
}).catch(() => false);
if (!actionsuccess) return false;
}
},

async ladderupdate(params) {
const server = await this.getServer(true);
if (server?.id !== Config.mainserver) {
Expand Down
31 changes: 29 additions & 2 deletions src/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class Session {
}
return this.login(username, password);
}
async login(name: string, pass: string) {
async login(name: string, pass: string, mfa?: string) {
const curTime = time();
await this.logout();
const userid = toID(name);
Expand All @@ -144,11 +144,27 @@ export class Session {
// unregistered. just do the thing
return this.context.user.login(name);
}
// if 2FA is enabled...
if (info.mfaenabled) {
// if no MFA token is provided, throw error
if (!mfa) {
throw new ActionError('Wrong authentication code.');
}
const mfaverified = await this.mfaVerify(name, mfa);
// if incorrect MFA token is provided, throw error
if (!mfaverified) {
throw new ActionError('Wrong authentication code.');
}
}
// previously, there was a case for banstate here in the php.
// this is not necessary, as getAssertion handles that. Proceed to verification.
const verified = await this.passwordVerify(name, pass);
if (!verified) {
throw new ActionError('Wrong password.');
if (info.mfaenabled) {
throw new ActionError('Wrong password. Please re-enter your authentication code.');
} else {
throw new ActionError('Wrong password.');
}
}
const timeout = (curTime + SID_DURATION);
const ip = this.context.getIp();
Expand Down Expand Up @@ -248,6 +264,9 @@ export class Session {
if (userstate.email?.endsWith('@')) {
return ';;@gmail';
}
if (userstate.mfaenabled) {
return ';;mfa';
}
return ';';
} else {
// Unregistered username.
Expand Down Expand Up @@ -427,6 +446,14 @@ export class Session {
return await this.checkLoggedIn() ??
new User(this.cookies.get('showdown_username'));
}
async mfaVerify(name: string, token: string) {
const userid = toID(name);
const userData = await users.get('*', userid);
if (userData?.mfatoken) {
const mfatoken = userData?.mfatoken;
return await verify(token, mfatoken);
}
}
async checkLoggedIn() {
const ctime = time();
const body = this.context.body;
Expand Down