2017-05-25 20:09:29 +07:00
|
|
|
|
|
|
|
import objectPath = require("object-path");
|
|
|
|
import randomstring = require("randomstring");
|
|
|
|
import BluebirdPromise = require("bluebird");
|
|
|
|
import util = require("util");
|
|
|
|
import Exceptions = require("./Exceptions");
|
|
|
|
import fs = require("fs");
|
|
|
|
import ejs = require("ejs");
|
2017-07-20 02:06:12 +07:00
|
|
|
import { IUserDataStore } from "./storage/IUserDataStore";
|
2017-05-25 20:09:29 +07:00
|
|
|
import { Winston } from "../../types/Dependencies";
|
|
|
|
import express = require("express");
|
|
|
|
import ErrorReplies = require("./ErrorReplies");
|
2017-09-22 03:07:34 +07:00
|
|
|
import { ServerVariablesHandler } from "./ServerVariablesHandler";
|
2017-05-25 20:09:29 +07:00
|
|
|
import AuthenticationSession = require("./AuthenticationSession");
|
|
|
|
|
|
|
|
import Identity = require("../../types/Identity");
|
2017-07-20 02:06:12 +07:00
|
|
|
import { IdentityValidationDocument } from "./storage/IdentityValidationDocument";
|
2017-05-25 20:09:29 +07:00
|
|
|
|
|
|
|
const filePath = __dirname + "/../resources/email-template.ejs";
|
|
|
|
const email_template = fs.readFileSync(filePath, "utf8");
|
|
|
|
|
|
|
|
// IdentityValidator allows user to go through a identity validation process in two steps:
|
|
|
|
// - Request an operation to be performed (password reset, registration).
|
|
|
|
// - Confirm operation with email.
|
|
|
|
|
|
|
|
export interface IdentityValidable {
|
|
|
|
challenge(): string;
|
|
|
|
preValidationInit(req: express.Request): BluebirdPromise<Identity.Identity>;
|
|
|
|
postValidationInit(req: express.Request): BluebirdPromise<void>;
|
|
|
|
|
|
|
|
preValidationResponse(req: express.Request, res: express.Response): void; // Serves a page after identity check request
|
|
|
|
postValidationResponse(req: express.Request, res: express.Response): void; // Serves the page if identity validated
|
|
|
|
mailSubject(): string;
|
|
|
|
}
|
|
|
|
|
2017-07-20 02:06:12 +07:00
|
|
|
function createAndSaveToken(userid: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<string> {
|
2017-05-25 20:09:29 +07:00
|
|
|
const five_minutes = 4 * 60 * 1000;
|
|
|
|
const token = randomstring.generate({ length: 64 });
|
|
|
|
const that = this;
|
|
|
|
|
|
|
|
logger.debug("identity_check: issue identity token %s for 5 minutes", token);
|
2017-07-20 02:06:12 +07:00
|
|
|
return userDataStore.produceIdentityValidationToken(userid, token, challenge, five_minutes)
|
2017-05-25 20:09:29 +07:00
|
|
|
.then(function () {
|
|
|
|
return BluebirdPromise.resolve(token);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-07-20 02:06:12 +07:00
|
|
|
function consumeToken(token: string, challenge: string, userDataStore: IUserDataStore, logger: Winston): BluebirdPromise<IdentityValidationDocument> {
|
2017-05-25 20:09:29 +07:00
|
|
|
logger.debug("identity_check: consume token %s", token);
|
2017-07-20 02:06:12 +07:00
|
|
|
return userDataStore.consumeIdentityValidationToken(token, challenge);
|
2017-05-25 20:09:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function register(app: express.Application, pre_validation_endpoint: string, post_validation_endpoint: string, handler: IdentityValidable) {
|
|
|
|
app.get(pre_validation_endpoint, get_start_validation(handler, post_validation_endpoint));
|
|
|
|
app.get(post_validation_endpoint, get_finish_validation(handler));
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkIdentityToken(req: express.Request, identityToken: string): BluebirdPromise<void> {
|
|
|
|
if (!identityToken)
|
|
|
|
return BluebirdPromise.reject(new Exceptions.AccessDeniedError("No identity token provided"));
|
|
|
|
return BluebirdPromise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function get_finish_validation(handler: IdentityValidable): express.RequestHandler {
|
|
|
|
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
|
2017-07-20 02:06:12 +07:00
|
|
|
const logger = ServerVariablesHandler.getLogger(req.app);
|
|
|
|
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
|
2017-05-25 20:09:29 +07:00
|
|
|
|
2017-09-22 03:07:34 +07:00
|
|
|
let authSession: AuthenticationSession.AuthenticationSession;
|
2017-05-25 20:09:29 +07:00
|
|
|
const identityToken = objectPath.get<express.Request, string>(req, "query.identity_token");
|
|
|
|
logger.info("GET identity_check: identity token provided is %s", identityToken);
|
|
|
|
|
|
|
|
return checkIdentityToken(req, identityToken)
|
|
|
|
.then(function () {
|
|
|
|
return handler.postValidationInit(req);
|
|
|
|
})
|
2017-09-22 03:07:34 +07:00
|
|
|
.then(function () {
|
|
|
|
return AuthenticationSession.get(req);
|
|
|
|
})
|
|
|
|
.then(function (_authSession: AuthenticationSession.AuthenticationSession) {
|
|
|
|
authSession = _authSession;
|
|
|
|
})
|
2017-05-25 20:09:29 +07:00
|
|
|
.then(function () {
|
2017-07-20 02:06:12 +07:00
|
|
|
return consumeToken(identityToken, handler.challenge(), userDataStore, logger);
|
2017-05-25 20:09:29 +07:00
|
|
|
})
|
2017-07-20 02:06:12 +07:00
|
|
|
.then(function (doc: IdentityValidationDocument) {
|
2017-05-25 20:09:29 +07:00
|
|
|
authSession.identity_check = {
|
|
|
|
challenge: handler.challenge(),
|
2017-07-20 02:06:12 +07:00
|
|
|
userid: doc.userId
|
2017-05-25 20:09:29 +07:00
|
|
|
};
|
|
|
|
handler.postValidationResponse(req, res);
|
|
|
|
return BluebirdPromise.resolve();
|
|
|
|
})
|
|
|
|
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
|
|
|
|
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
|
|
|
|
.catch(ErrorReplies.replyWithError500(res, logger));
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string): express.RequestHandler {
|
|
|
|
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
|
2017-07-20 02:06:12 +07:00
|
|
|
const logger = ServerVariablesHandler.getLogger(req.app);
|
|
|
|
const notifier = ServerVariablesHandler.getNotifier(req.app);
|
|
|
|
const userDataStore = ServerVariablesHandler.getUserDataStore(req.app);
|
2017-05-25 20:09:29 +07:00
|
|
|
let identity: Identity.Identity;
|
|
|
|
logger.info("Identity Validation: Start identity validation");
|
|
|
|
|
|
|
|
return handler.preValidationInit(req)
|
|
|
|
.then(function (id: Identity.Identity) {
|
|
|
|
logger.debug("Identity Validation: retrieved identity is %s", JSON.stringify(id));
|
|
|
|
identity = id;
|
2017-07-20 02:06:12 +07:00
|
|
|
const email = identity.email;
|
|
|
|
const userid = identity.userid;
|
2017-05-25 20:09:29 +07:00
|
|
|
|
2017-07-20 02:06:12 +07:00
|
|
|
if (!(email && userid))
|
2017-05-25 20:09:29 +07:00
|
|
|
return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address"));
|
|
|
|
|
2017-07-20 02:06:12 +07:00
|
|
|
return createAndSaveToken(userid, handler.challenge(), userDataStore, logger);
|
2017-05-25 20:09:29 +07:00
|
|
|
})
|
|
|
|
.then(function (token: string) {
|
|
|
|
const host = req.get("Host");
|
|
|
|
const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token);
|
|
|
|
logger.info("POST identity_check: notification sent to user %s", identity.userid);
|
|
|
|
return notifier.notify(identity, handler.mailSubject(), link_url);
|
|
|
|
})
|
|
|
|
.then(function () {
|
|
|
|
handler.preValidationResponse(req, res);
|
|
|
|
return BluebirdPromise.resolve();
|
|
|
|
})
|
|
|
|
.catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(res, logger))
|
|
|
|
.catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(res, logger))
|
|
|
|
.catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(res, logger))
|
|
|
|
.catch(ErrorReplies.replyWithError500(res, logger));
|
|
|
|
};
|
|
|
|
}
|